diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/form-control-on-second-page.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/form-control-on-second-page.pdf
new file mode 100644
index 000000000..d3dacc9da
Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/form-control-on-second-page.pdf differ
diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/form-signature-field.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/form-signature-field.pdf
new file mode 100644
index 000000000..eadc3a117
Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/form-signature-field.pdf differ
diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/form-control-on-second-page.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/form-control-on-second-page.html
new file mode 100644
index 000000000..da34fe79b
--- /dev/null
+++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/form-control-on-second-page.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/form-signature-field.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/form-signature-field.html
new file mode 100644
index 000000000..3c53c980d
--- /dev/null
+++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/form-signature-field.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
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 60c1cfef0..4bebdc96c 100644
--- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java
+++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java
@@ -1,11 +1,10 @@
package com.openhtmltopdf.nonvisualregressiontests;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.hasItem;
+import static org.junit.Assert.*;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
@@ -29,6 +28,7 @@
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
+import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
@@ -74,14 +74,14 @@ public static void configure() {
private static void render(String fileName, String html, BuilderConfig config) throws IOException {
ByteArrayOutputStream actual = new ByteArrayOutputStream();
-
+
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.withHtmlContent(html, NonVisualRegressionTest.class.getResource(RES_PATH).toString());
builder.toStream(actual);
builder.useFastMode();
builder.testMode(true);
config.configure(builder);
-
+
try {
builder.run();
} catch (Exception e) {
@@ -101,7 +101,7 @@ private static String loadHtml(String fileName) throws IOException {
try (InputStream is = TestcaseRunner.class.getResourceAsStream(absResPath)) {
byte[] htmlBytes = IOUtils
- .toByteArray(is);
+ .toByteArray(is);
return new String(htmlBytes, StandardCharsets.UTF_8);
}
@@ -116,7 +116,8 @@ private static PDDocument run(String fileName, BuilderConfig config) throws IOEx
}
private static PDDocument run(String filename) throws IOException {
- return run(filename, b -> {});
+ return run(filename, b -> {
+ });
}
private static PDDocument load(String filename) throws IOException {
@@ -131,7 +132,15 @@ private static void remove(String fileName, PDDocument doc) throws IOException {
private static double cssPixelsToPdfPoints(double cssPixels) {
return cssPixels * 72d / 96d;
}
-
+
+ private static double cssPixelsYToPdfPoints(double cssPixels, double cssPixelsPageHeight) {
+ return cssPixelsPageHeight - cssPixelsToPdfPoints(cssPixels);
+ }
+
+ private static double pdfPointsToCssPixels(double pdfPoints) {
+ return pdfPoints * 96d / 72d;
+ }
+
private static double cssPixelYToPdfPoints(double cssPixelsY, double cssPixelsPageHeight) {
return cssPixelsToPdfPoints(cssPixelsPageHeight - cssPixelsY);
}
@@ -139,22 +148,25 @@ private static double cssPixelYToPdfPoints(double cssPixelsY, double cssPixelsPa
private static class RectangleCompare extends CustomTypeSafeMatcher
{
private final PDRectangle expec;
private final double pageHeight;
-
+
private RectangleCompare(PDRectangle expected, double pageHeight) {
super("Compare Rectangles");
this.expec = expected;
this.pageHeight = pageHeight;
}
-
+
@Override
protected boolean matchesSafely(PDRectangle item) {
- assertEquals(cssPixelsToPdfPoints(this.expec.getLowerLeftX()), item.getLowerLeftX(), 1.0d);
- assertEquals(cssPixelsToPdfPoints(this.expec.getUpperRightX()), item.getUpperRightX(), 1.0d);
-
+ String actualInCssPixels = "[" + pdfPointsToCssPixels(item.getLowerLeftX()) + "," + cssPixelsYToPdfPoints(item.getLowerLeftY(), pageHeight) + "," +
+ pdfPointsToCssPixels(item.getUpperRightX()) + "," + cssPixelsYToPdfPoints(item.getUpperRightY(), pageHeight) + "]";
+ String message = "Dimensions do not match expected: " + this.expec.toString() + " actual: " + actualInCssPixels;
+ assertEquals(message, cssPixelsToPdfPoints(this.expec.getLowerLeftX()), item.getLowerLeftX(), 1.0d);
+ assertEquals(message, cssPixelsToPdfPoints(this.expec.getUpperRightX()), item.getUpperRightX(), 1.0d);
+
// Note: We swap the Ys here because PDFBOX returns a rect in bottom up units while expected is in topdown units.
- assertEquals(cssPixelYToPdfPoints(this.expec.getUpperRightY(), pageHeight), item.getLowerLeftY(), 1.0d);
- assertEquals(cssPixelYToPdfPoints(this.expec.getLowerLeftY(), pageHeight), item.getUpperRightY(), 1.0d);
-
+ assertEquals(message, cssPixelYToPdfPoints(this.expec.getUpperRightY(), pageHeight), item.getLowerLeftY(), 1.0d);
+ assertEquals(message, cssPixelYToPdfPoints(this.expec.getLowerLeftY(), pageHeight), item.getUpperRightY(), 1.0d);
+
return true;
}
}
@@ -184,7 +196,7 @@ public void testMetaInformation() throws IOException {
}
/**
- * Tests that a simple head bookmark linking to top of the second page works.
+ * Tests that a simple head bookmark linking to top of the second page works.
*/
@Test
public void testBookmarkHeadSimple() throws IOException {
@@ -226,7 +238,7 @@ public void testBookmarkBodySimple() throws IOException {
}
/**
- * Tests that a head bookmark linking to transformed element (by way of transform) on third page works.
+ * Tests that a head bookmark linking to transformed element (by way of transform) on third page works.
*/
@Test
public void testBookmarkHeadTransform() throws IOException {
@@ -245,9 +257,9 @@ public void testBookmarkHeadTransform() throws IOException {
remove("bookmark-head-transform", doc);
}
}
-
+
/**
- * Tests that a head bookmark linking to element (on overflow page).
+ * Tests that a head bookmark linking to element (on overflow page).
*/
@Test
public void testBookmarkHeadOnOverflowPage() throws IOException {
@@ -268,7 +280,7 @@ public void testBookmarkHeadOnOverflowPage() throws IOException {
}
/**
- * Tests that a head bookmark linking to an inline element (on page after overflow page) works.
+ * Tests that a head bookmark linking to an inline element (on page after overflow page) works.
*/
@Test
public void testBookmarkHeadAfterOverflowPage() throws IOException {
@@ -287,9 +299,9 @@ public void testBookmarkHeadAfterOverflowPage() throws IOException {
remove("bookmark-head-after-overflow-page", doc);
}
}
-
+
/**
- * Tests that a nested head bookmark linking to top of the third page works.
+ * Tests that a nested head bookmark linking to top of the third page works.
*/
@Test
public void testBookmarkHeadNested() throws IOException {
@@ -338,7 +350,7 @@ public void testIssue364InvalidFootnoteContent() throws IOException {
* + Pseudos (::footnote-call, ::footnote-marker, ::before, ::after) with float: footnote.
* + Pseudos in footnotes with position: fixed.
* + Invalid styles in the footnote at-rule such as position: fixed.
- *
+ *
* Primarily to check that these scenarios do not cause infinite loop
* or out-of-memory and ideally don't throw exceptions.
* Bad footnote content is not supported and will not produce expected results.
@@ -381,7 +393,7 @@ public void testFormControlText() throws IOException {
remove("form-control-text", doc);
}
}
-
+
/**
* Tests the positioning, size, name and value of a form control on an overflow page.
*/
@@ -404,10 +416,51 @@ public void testFormControlOverflowPage() throws IOException {
assertEquals("text-input", field.getFullyQualifiedName());
assertEquals("Hello World!", field.getValue());
+
remove("form-control-overflow-page", doc);
}
}
-
+
+ /**
+ * Tests the positioning, size, name and value of a form control on an overflow page.
+ */
+ @Test
+ public void testFormControlOnSecondPage() throws IOException {
+ try (PDDocument doc = run("form-control-on-second-page")) {
+
+ PDPage page0 = doc.getPage(0);
+ PDPage page1 = doc.getPage(1);
+ PDPage page2 = doc.getPage(2);
+
+ assertEquals(1, page0.getAnnotations().size());
+ assertEquals(1, page1.getAnnotations().size());
+ assertEquals(0, page2.getAnnotations().size());
+
+ assertThat(page0.getAnnotations().get(0), instanceOf(PDAnnotationWidget.class));
+ assertThat(page1.getAnnotations().get(0), instanceOf(PDAnnotationWidget.class));
+
+ PDRectangle rectangle0 = page0.getAnnotations().get(0).getRectangle();
+ assertTrue(page0.getMediaBox().contains(rectangle0.getLowerLeftX(), rectangle0.getLowerLeftY()));
+ assertTrue(page0.getMediaBox().contains(rectangle0.getUpperRightX(), rectangle0.getUpperRightY()));
+
+ PDRectangle rectangle1 = page1.getAnnotations().get(0).getRectangle();
+ assertTrue(page1.getMediaBox().contains(rectangle1.getLowerLeftX(), rectangle1.getLowerLeftY()));
+ assertTrue(page1.getMediaBox().contains(rectangle1.getUpperRightX(), rectangle1.getUpperRightY()));
+
+ PDAcroForm form = doc.getDocumentCatalog().getAcroForm();
+ assertEquals(2, form.getFields().size());
+ assertThat(form.getFields().get(0), instanceOf(PDTextField.class));
+ assertThat(form.getFields().get(1), instanceOf(PDTextField.class));
+
+ PDTextField field = (PDTextField) form.getFields().get(0);
+ assertEquals("Hello World!", field.getValue());
+ PDTextField field2 = (PDTextField) form.getFields().get(1);
+ assertEquals("Hello Second World!", field2.getValue());
+
+ remove("form-control-on-second-page", doc);
+ }
+ }
+
/**
* Tests the positioning, size, name and value of a form control appearing after an overflow page.
*/
@@ -439,7 +492,7 @@ public void testFormControlAfterOverflowPage() throws IOException {
* Check that an input without name attribute does not launch a NPE.
* Will now log a warning message.
* See issue: https://github.com/danfickle/openhtmltopdf/issues/151
- *
+ *
* Additionally, check that a select element without options will not launch a NPE too.
*/
@Test
@@ -485,6 +538,7 @@ private static String print(float[] floats) {
}
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");
@@ -524,24 +578,24 @@ public void testIssue458PageContentRepeatedInMargin() throws IOException {
PDFTextStripper stripper = new PDFTextStripper();
String text = stripper.getText(doc);
- String expected =
- IntStream.rangeClosed(1, 9)
- .mapToObj(i -> "Line " + i + "\r\n")
- .collect(Collectors.joining()) +
- "This is \r\n" +
- "some \r\n" +
- "flowing \r\n" +
- "text that \r\n" +
- "should not \r\n" +
- "repeat in \r\n" +
- "page \r\n" +
- "margins.\r\n" +
- "1. \r\n" +
- "2. \r\n" +
- "3. \r\n" +
- "One\r\n" +
- "Two\r\n" +
- "Three";
+ String expected =
+ IntStream.rangeClosed(1, 9)
+ .mapToObj(i -> "Line " + i + "\r\n")
+ .collect(Collectors.joining()) +
+ "This is \r\n" +
+ "some \r\n" +
+ "flowing \r\n" +
+ "text that \r\n" +
+ "should not \r\n" +
+ "repeat in \r\n" +
+ "page \r\n" +
+ "margins.\r\n" +
+ "1. \r\n" +
+ "2. \r\n" +
+ "3. \r\n" +
+ "One\r\n" +
+ "Two\r\n" +
+ "Three";
String normalizedExpected = expected.replaceAll("(\\r|\\n)", "");
String normalizedActual = text.replaceAll("(\\r|\\n)", "");
@@ -577,12 +631,12 @@ public void testPR480LinkShapes() throws IOException {
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 });
+ 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);
@@ -591,12 +645,12 @@ public void testPR480LinkShapes() throws IOException {
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 });
+ 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);
@@ -651,16 +705,16 @@ public void testIssue364MuchText() throws IOException {
@Test
public void testIssue364FootnotesDeepNesting() throws IOException {
Function deeper = (tag) ->
- IntStream.range(0, 50)
- .mapToObj(u -> tag)
- .collect(Collectors.joining());
-
- String[][] tags = new String[][] {
- { "", "
" },
- { "", "" },
- { "", "
" },
- { "", " | " },
- { "", "
" },
+ IntStream.range(0, 50)
+ .mapToObj(u -> tag)
+ .collect(Collectors.joining());
+
+ String[][] tags = new String[][]{
+ {"", "
"},
+ {"", ""},
+ {"", "
"},
+ {"", " | "},
+ {"", "
"},
};
StringBuilder sb = new StringBuilder();
@@ -710,7 +764,7 @@ private static void runFuzzTest(String html, boolean useFont) throws IOException
*/
private static void createCombinationTest(
StringBuilder sb, int widthPx, String whiteSpace, String wordWrap, List all, Random rndm, int testCharCount) {
- String start = String.format(Locale.US, "",
+ String start = String.format(Locale.US, "
",
whiteSpace, wordWrap, widthPx);
String end = "
";
@@ -738,34 +792,34 @@ private static void createCombinationTest(
* which have special meaning to the line breaking algorithms.
*/
private static List
createAllCombinations() {
- char[] chars = new char[] { 'x', '\u00ad', '\n', '\r', ' ' };
+ char[] chars = new char[]{'x', '\u00ad', '\n', '\r', ' '};
int[] loopIndices = new int[chars.length];
int totalCombinations = (int) Math.pow(loopIndices.length, loopIndices.length);
List ret = new ArrayList<>(totalCombinations);
for (int i = 0; i < totalCombinations; i++) {
- char[] result = new char[loopIndices.length];
+ char[] result = new char[loopIndices.length];
- for (int k = 0; k < loopIndices.length; k++) {
- char ch = chars[loopIndices[k]];
- result[k] = ch;
- }
+ for (int k = 0; k < loopIndices.length; k++) {
+ char ch = chars[loopIndices[k]];
+ result[k] = ch;
+ }
- ret.add(result);
+ ret.add(result);
- boolean carry = true;
- for (int j = loopIndices.length - 1; j >= 0; j--) {
- if (carry) {
- loopIndices[j]++;
- carry = false;
- }
+ boolean carry = true;
+ for (int j = loopIndices.length - 1; j >= 0; j--) {
+ if (carry) {
+ loopIndices[j]++;
+ carry = false;
+ }
- if (loopIndices[j] >= chars.length) {
- loopIndices[j] = 0;
- carry = true;
- }
+ if (loopIndices[j] >= chars.length) {
+ loopIndices[j] = 0;
+ carry = true;
}
+ }
}
return ret;
@@ -777,14 +831,14 @@ private static List createAllCombinations() {
*/
@Test
public void testPr492InfiniteLoopBugsInLineBreakingFuzz() throws IOException {
- final String[] whiteSpace = new String[] { "normal", "pre", "nowrap", "pre-wrap", "pre-line" };
- final String[] wordWrap = new String[] { "normal", "break-word" };
+ final String[] whiteSpace = new String[]{"normal", "pre", "nowrap", "pre-wrap", "pre-line"};
+ final String[] wordWrap = new String[]{"normal", "break-word"};
final List all = createAllCombinations();
final Random rndm = new Random();
long seed = rndm.nextLong();
System.out.println("For NonVisualRegressionTest::testPr492InfiniteLoopBugsInLineBreakingFuzz " +
- "using a random seed of " + seed + " for Random instance.");
+ "using a random seed of " + seed + " for Random instance.");
rndm.setSeed(seed);
List lengths = new ArrayList<>();
@@ -800,7 +854,7 @@ public void testPr492InfiniteLoopBugsInLineBreakingFuzz() throws IOException {
for (int j = 0; j < whiteSpace.length; j++) {
for (int k = 0; k < wordWrap.length; k++) {
for (Integer len : lengths) {
- createCombinationTest(sb, i, whiteSpace[j], wordWrap[k], all, rndm, len);
+ createCombinationTest(sb, i, whiteSpace[j], wordWrap[k], all, rndm, len);
}
}
}
@@ -828,16 +882,16 @@ public void testPr489DiagnosticConsumer() throws IOException {
}
Assert.assertTrue(
- logs.stream()
- .noneMatch(diag -> diag.getLogMessageId() == LogMessageId.LogMessageId1Param.EXCEPTION_CANT_READ_IMAGE_FILE_FOR_URI));
+ logs.stream()
+ .noneMatch(diag -> diag.getLogMessageId() == LogMessageId.LogMessageId1Param.EXCEPTION_CANT_READ_IMAGE_FILE_FOR_URI));
Assert.assertTrue(
- logs.stream()
- .anyMatch(diag -> diag.getLogMessageId() == LogMessageId.LogMessageId2Param.CSS_PARSE_GENERIC_MESSAGE));
+ logs.stream()
+ .anyMatch(diag -> diag.getLogMessageId() == LogMessageId.LogMessageId2Param.CSS_PARSE_GENERIC_MESSAGE));
Assert.assertTrue(
- logs.stream()
- .allMatch(diag -> !diag.getFormattedMessage().isEmpty()));
+ logs.stream()
+ .allMatch(diag -> !diag.getFormattedMessage().isEmpty()));
}
@Test
diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java
index 6fa141a88..471720166 100644
--- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java
+++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java
@@ -1492,6 +1492,15 @@ public void testIssue792TargetCounterStyle() throws IOException {
assertTrue(vt.runTest("issue-792-target-counter-style"));
}
+ @Test
+ public void testSignatureField() throws IOException {
+ assertTrue(vt.runTest("form-signature-field"));
+ }
+ @Test
+ public void testFormFieldOnSecondPage() throws IOException {
+ assertTrue(vt.runTest("form-control-on-second-page"));
+ }
+
// TODO:
// + Elements that appear just on generated overflow pages.
// + content property (page counters, etc)
diff --git a/openhtmltopdf-java2d/pom.xml b/openhtmltopdf-java2d/pom.xml
index aba87bbff..1f4032bdf 100644
--- a/openhtmltopdf-java2d/pom.xml
+++ b/openhtmltopdf-java2d/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-java2d
diff --git a/openhtmltopdf-latex-support/pom.xml b/openhtmltopdf-latex-support/pom.xml
index 2f17c3cbc..0cfb93eac 100644
--- a/openhtmltopdf-latex-support/pom.xml
+++ b/openhtmltopdf-latex-support/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-latex-support
diff --git a/openhtmltopdf-mathml-support/pom.xml b/openhtmltopdf-mathml-support/pom.xml
index 07ca10a30..75be6c3ca 100644
--- a/openhtmltopdf-mathml-support/pom.xml
+++ b/openhtmltopdf-mathml-support/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-mathml-support
diff --git a/openhtmltopdf-objects/pom.xml b/openhtmltopdf-objects/pom.xml
index db86485bf..a837359e9 100644
--- a/openhtmltopdf-objects/pom.xml
+++ b/openhtmltopdf-objects/pom.xml
@@ -18,7 +18,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-objects
diff --git a/openhtmltopdf-pdfa-testing/pom.xml b/openhtmltopdf-pdfa-testing/pom.xml
index 134afdc12..5757b6100 100644
--- a/openhtmltopdf-pdfa-testing/pom.xml
+++ b/openhtmltopdf-pdfa-testing/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-pdfa-testing
diff --git a/openhtmltopdf-pdfbox/pom.xml b/openhtmltopdf-pdfbox/pom.xml
index da5cec5b6..e470954bd 100644
--- a/openhtmltopdf-pdfbox/pom.xml
+++ b/openhtmltopdf-pdfbox/pom.xml
@@ -18,7 +18,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-pdfbox
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/DOMUtil.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/DOMUtil.java
index dc509d0e9..ff8c92266 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/DOMUtil.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/DOMUtil.java
@@ -21,7 +21,9 @@
import java.util.ArrayList;
import java.util.List;
+
import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -31,7 +33,7 @@ public static Element getChild(Element parent, String name) {
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
- Element elem = (Element)n;
+ Element elem = (Element) n;
if (elem.getTagName().equals(name)) {
return elem;
}
@@ -39,14 +41,14 @@ public static Element getChild(Element parent, String name) {
}
return null;
}
-
+
public static List getChildren(Element parent, String name) {
List result = new ArrayList<>();
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
- Element elem = (Element)n;
+ Element elem = (Element) n;
if (elem.getTagName().equals(name)) {
result.add(elem);
}
@@ -54,7 +56,7 @@ public static List getChildren(Element parent, String name) {
}
return result.size() == 0 ? null : result;
}
-
+
/**
* Helper function to find an enclosing element with given node name. Returns null on failure.
*/
@@ -62,14 +64,14 @@ public static Element findClosestEnclosingElementWithNodeName(Node e, String nod
Node parent;
while ((parent = e.getParentNode()) != null) {
if (parent.getNodeType() == Node.ELEMENT_NODE &&
- parent.getNodeName().equals(nodeName)) {
+ parent.getNodeName().equals(nodeName)) {
return (Element) parent;
}
e = parent;
}
return null;
}
-
+
/**
* Loads all of the text content in all offspring of an element.
* Ignores all attributes, comments and processing instructions.
@@ -77,11 +79,11 @@ public static Element findClosestEnclosingElementWithNodeName(Node e, String nod
* @return a String with the text content of an element (may be an empty string but will not be null).
*/
public static String getText(Element parent) {
- StringBuilder sb = new StringBuilder();
- getText(parent, sb);
+ StringBuilder sb = new StringBuilder();
+ getText(parent, sb);
return sb.toString();
}
-
+
/**
* Appends all text content in all offspring of an element to a StringBuffer.
* Ignores all attributes, comments and processing instructions.
@@ -93,10 +95,28 @@ public static void getText(Element parent, StringBuilder sb) {
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
- getText((Element)n, sb);
+ getText((Element) n, sb);
} else if (n.getNodeType() == Node.TEXT_NODE) {
sb.append(n.getNodeValue());
}
}
}
+
+ public static String toDebugInfo(Element element) {
+ if (element == null)
+ return "null";
+ StringBuilder elementString = new StringBuilder();
+ elementString.append('<');
+ elementString.append(element.getNodeName());
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0; i < attributes.getLength(); i++) {
+ Node attribute = attributes.item(i);
+ elementString.append(' ');
+ elementString.append(attribute.getNodeName());
+ elementString.append("=\"");
+ elementString.append(attribute.getNodeValue());
+ elementString.append('"');
+ }
+ return elementString.toString();
+ }
}
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java
index 0258e9cbd..3dac7c056 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java
@@ -46,11 +46,11 @@ public class PdfBoxAccessibilityHelper {
private final Box _rootBox;
private final Document _doc;
private final GenericStructualElement _root;
-
+
private static final Map> _tagSuppliers = createTagSuppliers();
-
+
private int _nextMcid;
-
+
// These change with every page, most of them are only needed while we paint the page, so we don't add
// them to the PageItems class, which is stored for later processing.
private PageItems _pageItems;
@@ -59,27 +59,27 @@ public class PdfBoxAccessibilityHelper {
private PDPage _page;
private float _pageHeight;
private AffineTransform _transform;
-
+
private int _runningLevel;
-
+
private static Map> createTagSuppliers() {
Map> suppliers = new HashMap<>();
-
+
suppliers.put("ul", ListStructualElement::new);
suppliers.put("ol", ListStructualElement::new);
suppliers.put("li", ListItemStructualElement::new);
-
+
suppliers.put("table", TableStructualElement::new);
suppliers.put("tr", TableRowStructualElement::new);
suppliers.put("td", TableCellStructualElement::new);
suppliers.put("th", TableHeaderStructualElement::new);
-
+
suppliers.put("a", AnchorStuctualElement::new);
suppliers.put("abbr", AbbrStuctualElement::new);
-
+
return suppliers;
}
-
+
public PdfBoxAccessibilityHelper(PdfBoxFastOutputDevice od, Box root, Document doc) {
this._od = od;
this._rootBox = root;
@@ -88,12 +88,12 @@ public PdfBoxAccessibilityHelper(PdfBoxFastOutputDevice od, Box root, Document d
this._root.box = root;
root.setAccessiblityObject(this._root);
}
-
+
private static class PageItems {
final List _contentItems = new ArrayList<>();
final List _pageAnnotations = new ArrayList<>();
}
-
+
/**
* Can be either a structure element or a content item.
*/
@@ -107,10 +107,10 @@ private static abstract class AbstractStructualElement extends AbstractTreeItem
PDStructureElement elem;
PDStructureElement parentElem; // May be different from this.parent.elem if we skip boxes in the tree.
PDPage page;
-
+
abstract void addChild(AbstractTreeItem child);
abstract String getPdfTag();
-
+
void createPdfStrucureElement(AbstractStructualElement parent, AbstractStructualElement child) {
child.parentElem = parent.elem;
child.elem = new PDStructureElement(child.getPdfTag(), child.parentElem);
@@ -119,7 +119,7 @@ void createPdfStrucureElement(AbstractStructualElement parent, AbstractStructual
child.parentElem.appendKid(child.elem);
}
-
+
/**
* Handles globally valid HTML attributes such as title and lang.
*/
@@ -127,7 +127,7 @@ void handleGlobalAttributes() {
handleLangAttribute();
handleTitleAttribute();
}
-
+
void handleLangAttribute() {
if (box != null && box.getElement() != null) {
String lang = box.getElement().getAttribute("lang");
@@ -136,7 +136,7 @@ void handleLangAttribute() {
}
}
}
-
+
void handleTitleAttribute() {
if (box != null && box.getElement() != null) {
String alternate = box.getElement().getAttribute("title");
@@ -145,13 +145,13 @@ void handleTitleAttribute() {
}
}
}
-
+
/**
* Only a couple of types of structural elements need the PDF version
* so leave empty in the base class.
*/
void setPdfVersion(float version) { }
-
+
/**
* The optional attribute dictionary is used for additional information about
* the structural element such as bounding box, cell spans, etc.
@@ -165,13 +165,13 @@ void setAttributeDictionary(COSDictionary attrDict) {
// to crash.
this.elem.getCOSObject().setItem(COSName.A, attrDict);
}
-
+
@Override
public String toString() {
return String.format("[Structual Element-%s:%s]", super.toString(), box);
}
}
-
+
private static class GenericStructualElement extends AbstractStructualElement {
final List children = new ArrayList<>();
@@ -235,7 +235,7 @@ void finish(AbstractStructualElement parent) {
// A structual element such as Div, Sect, p, etc
// which contains other structual elements or content items (text).
GenericStructualElement child = this;
-
+
if (child.children.isEmpty() &&
(child.box.getElement() == null || !child.box.getElement().hasAttribute("id"))) {
// There is no point in outputting empty structual elements.
@@ -243,7 +243,7 @@ void finish(AbstractStructualElement parent) {
// use as a link or bookmark destination.
return;
}
-
+
if (child.box instanceof LineBox ||
(child.box instanceof InlineLayoutBox &&
child.children.size() == 1 &&
@@ -254,7 +254,7 @@ void finish(AbstractStructualElement parent) {
finishTreeItems(child.children, parent);
} else {
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
// Recursively, depth first, process the structual tree.
@@ -262,34 +262,34 @@ void finish(AbstractStructualElement parent) {
}
}
}
-
+
private static class AnchorStuctualElement extends GenericStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.LINK;
}
-
+
@Override
void finish(AbstractStructualElement parent) {
AnchorStuctualElement child = this;
createPdfStrucureElement(parent, child);
-
+
String alternate = box.getElement() != null ? box.getElement().getAttribute("title") : "";
if (alternate.isEmpty()) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId1Param.GENERAL_PDF_ACCESSIBILITY_NO_TITLE_TEXT_PROVIDED_FOR, "link");
}
child.elem.setAlternateDescription(alternate);
-
+
handleLangAttribute();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class AbbrStuctualElement extends GenericStructualElement {
float pdfVersion = 1.5f;
-
+
@Override
String getPdfTag() {
return StandardStructureTypes.SPAN;
@@ -313,19 +313,19 @@ void finish(AbstractStructualElement parent) {
} else {
XRLog.log(Level.INFO, LogMessageId.LogMessageId1Param.GENERAL_PDF_ACCESSIBILITY_NO_TITLE_TEXT_PROVIDED_FOR, "abbr tag");
}
-
+
handleLangAttribute();
}
finishTreeItems(child.children, child);
}
-
+
@Override
void setPdfVersion(float version) {
this.pdfVersion = version;
}
}
-
+
private static class ListStructualElement extends AbstractStructualElement {
final List listItems = new ArrayList<>();
@@ -337,7 +337,7 @@ String getPdfTag() {
@Override
void addChild(AbstractTreeItem child) {
if (child instanceof ListItemStructualElement) {
- this.listItems.add((ListItemStructualElement) child);
+ this.listItems.add((ListItemStructualElement) child);
} else {
logIncompatibleChild(this, child, ListItemStructualElement.class);
}
@@ -346,12 +346,12 @@ void addChild(AbstractTreeItem child) {
@Override
void finish(AbstractStructualElement parent) {
ListStructualElement child = this;
-
+
createPdfStrucureElement(parent, child);
IdentValue listStyleType = child.box.getStyle().getIdent(CSSName.LIST_STYLE_TYPE);
String listType;
-
+
if (listStyleType == IdentValue.NONE) {
listType = "None";
} else if (listStyleType == IdentValue.DISC) {
@@ -375,14 +375,14 @@ void finish(AbstractStructualElement parent) {
// Armenian, Georgian, Latin and Greek are not supported by the PDF spec.
listType = "Decimal";
}
-
+
COSDictionary listNumbering = new COSDictionary();
listNumbering.setItem(COSName.O, COSName.getPDFName("List"));
listNumbering.setItem(COSName.getPDFName("ListNumbering"), COSName.getPDFName(listType));
setAttributeDictionary(listNumbering);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.listItems, child);
}
}
@@ -390,15 +390,15 @@ void finish(AbstractStructualElement parent) {
private static class ListItemStructualElement extends AbstractStructualElement {
final ListLabelStructualElement label;
final ListBodyStructualElement body;
-
+
ListItemStructualElement() {
this.body = new ListBodyStructualElement();
this.body.parent = this;
-
+
this.label = new ListLabelStructualElement();
this.label.parent = this;
}
-
+
@Override
String getPdfTag() {
return StandardStructureTypes.LI;
@@ -412,9 +412,9 @@ void addChild(AbstractTreeItem child) {
@Override
void finish(AbstractStructualElement parent) {
ListItemStructualElement child = this;
-
+
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
finishTreeItem(child.label, child);
@@ -424,7 +424,7 @@ void finish(AbstractStructualElement parent) {
private static class ListLabelStructualElement extends AbstractStructualElement {
final List children = new ArrayList<>(1);
-
+
@Override
String getPdfTag() {
return StandardStructureTypes.LBL;
@@ -443,39 +443,39 @@ void finish(AbstractStructualElement parent) {
// Must be list-style-type: none.
return;
}
-
+
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
finishTreeItems(child.children, child);
}
}
-
+
private static class ListBodyStructualElement extends GenericStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.L_BODY;
}
-
+
@Override
void finish(AbstractStructualElement parent) {
ListBodyStructualElement child = this;
-
+
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class TableStructualElement extends AbstractStructualElement {
final TableHeadStructualElement thead = new TableHeadStructualElement();
final List tbodies = new ArrayList<>(1);
final TableFootStructualElement tfoot = new TableFootStructualElement();
float pdfVersion = 1.5f;
-
+
@Override
void addChild(AbstractTreeItem child) {
if (child instanceof TableBodyStructualElement) {
@@ -489,7 +489,7 @@ void addChild(AbstractTreeItem child) {
void setPdfVersion(float version) {
this.pdfVersion = version;
}
-
+
@Override
String getPdfTag() {
return StandardStructureTypes.TABLE;
@@ -498,11 +498,11 @@ String getPdfTag() {
@Override
void finish(AbstractStructualElement parent) {
TableStructualElement child = this;
-
+
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
-
+
if (pdfVersion < 1.5f) {
// THead, TBody and TFoot were introduced in PDF 1.5 so if we can not use
// them then we process their rows directly and add them to the table.
@@ -516,13 +516,13 @@ void finish(AbstractStructualElement parent) {
}
}
}
-
+
private static class TableHeadStructualElement extends GenericStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.T_HEAD;
}
-
+
@Override
void addChild(AbstractTreeItem child) {
if (!(child instanceof TableRowStructualElement)) {
@@ -530,24 +530,24 @@ void addChild(AbstractTreeItem child) {
}
super.addChild(child);
}
-
+
@Override
void finish(AbstractStructualElement parent) {
TableHeadStructualElement child = this;
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class TableBodyStructualElement extends GenericStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.T_BODY;
}
-
+
@Override
void addChild(AbstractTreeItem child) {
if (!(child instanceof TableRowStructualElement)) {
@@ -555,24 +555,24 @@ void addChild(AbstractTreeItem child) {
}
super.addChild(child);
}
-
+
@Override
void finish(AbstractStructualElement parent) {
TableBodyStructualElement child = this;
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class TableFootStructualElement extends GenericStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.T_FOOT;
}
-
+
@Override
void addChild(AbstractTreeItem child) {
if (!(child instanceof TableRowStructualElement)) {
@@ -580,24 +580,24 @@ void addChild(AbstractTreeItem child) {
}
super.addChild(child);
}
-
+
@Override
void finish(AbstractStructualElement parent) {
TableFootStructualElement child = this;
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class TableRowStructualElement extends GenericStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.TR;
}
-
+
@Override
void addChild(AbstractTreeItem child) {
if (!(child instanceof TableHeaderOrCellStructualElement)) {
@@ -605,102 +605,102 @@ void addChild(AbstractTreeItem child) {
}
super.addChild(child);
}
-
+
@Override
void finish(AbstractStructualElement parent) {
TableRowStructualElement child = this;
createPdfStrucureElement(parent, child);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static abstract class TableHeaderOrCellStructualElement extends GenericStructualElement {
boolean addCellAttributes(COSDictionary attrDict) {
TableHeaderOrCellStructualElement child = this;
-
+
boolean added = false;
int rowSpanAttr = 1;
int colSpanAttr = 1;
-
+
if (child.box instanceof TableCellBox) {
TableCellBox cell = (TableCellBox) box;
colSpanAttr = cell.getStyle().getColSpan();
rowSpanAttr = cell.getStyle().getRowSpan();
}
-
+
if (colSpanAttr != 1) {
added = true;
attrDict.setInt(COSName.getPDFName("ColSpan"), colSpanAttr);
}
-
+
if (rowSpanAttr != 1) {
added = true;
attrDict.setInt(COSName.getPDFName("RowSpan"), rowSpanAttr);
}
-
+
return added;
}
}
-
+
private static class TableHeaderStructualElement extends TableHeaderOrCellStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.TH;
}
-
+
@Override
void finish(AbstractStructualElement parent) {
TableHeaderStructualElement child = this;
createPdfStrucureElement(parent, child);
-
+
String scope = box.getElement() != null ? box.getElement().getAttribute("scope") : "";
-
+
COSDictionary attrDict = new COSDictionary();
attrDict.setItem(COSName.O, COSName.getPDFName("Table"));
-
+
if ("row".equals(scope)) {
attrDict.setItem(COSName.getPDFName("Scope"), COSName.getPDFName("Row"));
} else {
attrDict.setItem(COSName.getPDFName("Scope"), COSName.getPDFName("Column"));
}
-
+
addCellAttributes(attrDict);
setAttributeDictionary(attrDict);
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class TableCellStructualElement extends TableHeaderOrCellStructualElement {
@Override
String getPdfTag() {
return StandardStructureTypes.TD;
}
-
+
@Override
void finish(AbstractStructualElement parent) {
TableCellStructualElement child = this;
createPdfStrucureElement(parent, child);
-
+
COSDictionary attrDict = new COSDictionary();
attrDict.setItem(COSName.O, COSName.getPDFName("Table"));
-
+
if (addCellAttributes(attrDict)) {
setAttributeDictionary(attrDict);
}
-
+
handleGlobalAttributes();
-
+
finishTreeItems(child.children, child);
}
}
-
+
private static class FigureStructualElement extends AbstractStructualElement {
PDRectangle boundingBox;
FigureContentItem content;
@@ -723,19 +723,19 @@ void addChild(AbstractTreeItem child) {
void finish(AbstractStructualElement parent) {
// Must be a figure (image or replaced, etc).
FigureStructualElement child = this;
-
+
child.parentElem = parent.elem;
child.elem = new PDStructureElement(child.getPdfTag(), child.parentElem);
child.elem.setParent(child.parentElem);
child.elem.setPage(child.page);
-
+
// Add alt text.
String alternateText = child.box.getElement() == null ? "" : box.getElement().getAttribute("alt");
if (alternateText.isEmpty()) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId0Param.GENERAL_PDF_ACCESSIBILITY_NO_ALT_ATTRIBUTE_PROVIDED_FOR_IMAGE);
}
child.elem.setAlternateDescription(alternateText);
-
+
handleLangAttribute();
// Add bounding box attribute.
@@ -745,17 +745,17 @@ void finish(AbstractStructualElement parent) {
setAttributeDictionary(attributeDict);
child.parentElem.appendKid(child.elem);
-
+
finishTreeItem(child.content, child);
}
}
-
+
private static class GenericContentItem extends AbstractTreeItem {
PDStructureElement parentElem;
int mcid;
COSDictionary dict;
PDPage page;
-
+
@Override
public String toString() {
return String.format("[Content Item-%s:%d]", super.toString(), mcid);
@@ -766,8 +766,8 @@ void finish(AbstractStructualElement parent) {
// A content item (text or replaced image), we need to add it to its parent structual item.
GenericContentItem child = this;
boolean isReplaced = child instanceof FigureContentItem;
-
- if (child.page == parent.page) {
+
+ if (child.page == parent.page) {
// If this is on the same page as its parent structual element
// we can just use the dict with mcid in it only.
child.parentElem = parent.elem;
@@ -788,11 +788,11 @@ void finish(AbstractStructualElement parent) {
private static class FigureContentItem extends GenericContentItem {
}
-
+
private static void logIncompatibleChild(AbstractTreeItem parent, AbstractTreeItem child, Class> expected) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId3Param.GENERAL_PDF_ACCESSIBILITY_INCOMPATIBLE_CHILD, child.getClass().getSimpleName(), parent.getClass().getSimpleName(), expected.getSimpleName());
}
-
+
/**
* Given a box, gets its structual element.
*/
@@ -800,18 +800,18 @@ public static PDStructureElement getStructualElementForBox(Box targetBox) {
if (targetBox != null &&
targetBox.getAccessibilityObject() != null &&
targetBox.getAccessibilityObject() instanceof AbstractStructualElement) {
-
+
return ((AbstractStructualElement) targetBox.getAccessibilityObject()).elem;
}
-
+
return null;
}
-
+
public void finishPdfUa() {
PDStructureTreeRoot root = _od.getWriter().getDocumentCatalog().getStructureTreeRoot();
if (root == null) {
root = new PDStructureTreeRoot();
-
+
HashMap roleMap = new HashMap<>();
roleMap.put("Annotation", "Span");
roleMap.put("Artifact", "P");
@@ -830,40 +830,40 @@ public void finishPdfUa() {
root.setRoleMap(roleMap);
PDStructureElement rootElem = new PDStructureElement(StandardStructureTypes.DOCUMENT, null);
-
+
String lang = _doc.getDocumentElement().getAttribute("lang");
rootElem.setLanguage(lang.isEmpty() ? "EN-US" : lang);
-
+
root.appendKid(rootElem);
-
+
_root.elem = rootElem;
finishTreeItems(_root.children, _root);
-
+
_od.getWriter().getDocumentCatalog().setStructureTreeRoot(root);
}
}
-
+
public void finishNumberTree() {
COSArray numTree = new COSArray();
int i = 0;
-
+
for (Map.Entry entry : _pageItemsMap.entrySet()) {
List pageItems = entry.getValue()._contentItems;
List pageAnnotations = entry.getValue()._pageAnnotations;
COSArray mcidParentReferences = new COSArray();
-
+
for (GenericContentItem contentItem : pageItems) {
mcidParentReferences.add(contentItem.parentElem);
}
-
+
numTree.add(COSInteger.get(i));
numTree.add(mcidParentReferences);
-
+
entry.getKey().getCOSObject().setItem(COSName.STRUCT_PARENTS, COSInteger.get(i));
entry.getKey().getCOSObject().setItem(COSName.getPDFName("Tabs"), COSName.S);
i++;
-
+
for (AnnotationWithStructureParent annot : pageAnnotations) {
numTree.add(COSInteger.get(i));
numTree.add(annot.structureParent);
@@ -871,10 +871,10 @@ public void finishNumberTree() {
i++;
}
}
-
+
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.NUMS, numTree);
-
+
PDNumberTreeNode numberTreeNode = new PDNumberTreeNode(dict, dict.getClass());
_od.getWriter().getDocumentCatalog().getStructureTreeRoot().setParentTreeNextKey(i);
_od.getWriter().getDocumentCatalog().getStructureTreeRoot().setParentTree(numberTreeNode);
@@ -883,7 +883,7 @@ public void finishNumberTree() {
private static String guessBoxTag(Box box) {
if (box instanceof BlockBox) {
BlockBox block = (BlockBox) box;
-
+
if (block.isInline()) {
return StandardStructureTypes.SPAN;
} else {
@@ -893,13 +893,13 @@ private static String guessBoxTag(Box box) {
return StandardStructureTypes.SPAN;
}
}
-
+
private static void finishTreeItems(List extends AbstractTreeItem> children, AbstractStructualElement parent) {
for (AbstractTreeItem child : children) {
child.finish(parent);
}
}
-
+
private static void finishTreeItem(AbstractTreeItem item, AbstractStructualElement parent) {
item.finish(parent);
}
@@ -910,24 +910,24 @@ private COSDictionary createMarkedContentDictionary() {
_nextMcid++;
return dict;
}
-
+
private void ensureAncestorTree(AbstractTreeItem child, Box parent) {
// Walk up the ancestor tree making sure they all have accessibility objects.
while (parent != null && parent.getAccessibilityObject() == null) {
AbstractStructualElement parentItem = createStructureItem(null, parent);
parent.setAccessiblityObject(parentItem);
-
+
parentItem.addChild(child);
-
+
child.parent = parentItem;
child = parentItem;
parent = parent.getParent();
}
}
-
+
private AbstractStructualElement createStructureItem(StructureType type, Box box) {
AbstractStructualElement child = null;
-
+
if (box instanceof BlockBox) {
BlockBox bb = (BlockBox) box;
@@ -935,8 +935,8 @@ private AbstractStructualElement createStructureItem(StructureType type, Box box
// For replaced elements we will need to create a BBox.
// This is done here so we don'thave to hang onto the page height, transform, etc.
Rectangle2D rect = PdfBoxFastLinkManager.createTargetArea(
- _ctx, box, _pageHeight, _transform, _rootBox, _od);
-
+ _ctx, box, _pageHeight, _transform, _ctx.getPage(), _od);
+
child = new FigureStructualElement();
((FigureStructualElement) child).boundingBox = new PDRectangle(
(float) rect.getMinX(),
@@ -944,25 +944,25 @@ private AbstractStructualElement createStructureItem(StructureType type, Box box
(float) rect.getWidth(),
(float) rect.getHeight());
}
- }
-
+ }
+
if (child == null && box.getElement() != null && !box.isAnonymous()) {
String htmlTag = box.getElement().getTagName();
Supplier supplier = _tagSuppliers.get(htmlTag);
-
+
if (supplier != null) {
child = supplier.get();
}
}
-
+
if (child == null &&
box.getParent() != null &&
box.getParent().getAccessibilityObject() instanceof TableStructualElement) {
-
+
TableStructualElement table = (TableStructualElement) box.getParent().getAccessibilityObject();
-
+
table.setPdfVersion(_od.getWriter().getVersion());
-
+
if (box.getStyle().isIdent(CSSName.DISPLAY, IdentValue.TABLE_HEADER_GROUP)) {
child = table.thead;
} else if (box.getStyle().isIdent(CSSName.DISPLAY, IdentValue.TABLE_ROW_GROUP)) {
@@ -971,22 +971,22 @@ private AbstractStructualElement createStructureItem(StructureType type, Box box
child = table.tfoot;
}
}
-
-
+
+
if (child == null) {
child = new GenericStructualElement();
}
-
+
child.page = _page;
child.box = box;
child.setPdfVersion(_od.getWriter().getVersion());
-
+
return child;
}
-
+
private void setupStructureElement(AbstractStructualElement child, Box box) {
box.setAccessiblityObject(child);
-
+
ensureAncestorTree(child, box.getParent());
ensureParent(box, child);
}
@@ -1009,10 +1009,10 @@ private void ensureParent(Box box, AbstractTreeItem child) {
}
}
}
-
+
private GenericContentItem createMarkedContentStructureItem(StructureType type, Box box) {
GenericContentItem current = new GenericContentItem();
-
+
ensureAncestorTree(current, box.getParent());
AbstractStructualElement parent = (AbstractStructualElement) box.getAccessibilityObject();
@@ -1022,15 +1022,15 @@ private GenericContentItem createMarkedContentStructureItem(StructureType type,
current.mcid = _nextMcid;
current.dict = createMarkedContentDictionary();
current.page = _page;
-
+
_pageItems._contentItems.add(current);
return current;
}
-
+
private GenericContentItem createListItemLabelMarkedContent(StructureType type, Box box) {
GenericContentItem current = new GenericContentItem();
-
+
current.mcid = _nextMcid;
current.dict = createMarkedContentDictionary();
current.page = _page;
@@ -1038,66 +1038,66 @@ private GenericContentItem createListItemLabelMarkedContent(StructureType type,
ListItemStructualElement li = (ListItemStructualElement) box.getAccessibilityObject();
li.label.addChild(current);
current.parent = li.label;
-
+
_pageItems._contentItems.add(current);
return current;
}
-
+
private FigureContentItem createFigureContentStructureItem(StructureType type, Box box) {
FigureStructualElement parent = (FigureStructualElement) box.getAccessibilityObject();
-
+
if (parent == null ||
parent.content != null) {
// This figure structual element already has an image associatted with it.
// Images continued on subsequent pages will be treated as artifacts.
return null;
}
-
+
FigureContentItem current = new FigureContentItem();
-
+
ensureAncestorTree(current, box.getParent());
-
+
current.parent = parent;
current.mcid = _nextMcid;
current.dict = createMarkedContentDictionary();
current.page = _page;
parent.content = current;
-
+
_pageItems._contentItems.add(current);
-
+
return current;
}
-
+
private COSDictionary createBackgroundArtifact(StructureType type, Box box) {
- Rectangle2D rect = PdfBoxFastLinkManager.createTargetArea(_ctx, box, _pageHeight, _transform, _rootBox, _od);
+ Rectangle2D rect = PdfBoxFastLinkManager.createTargetArea(_ctx, box, _pageHeight, _transform, _ctx.getPage(), _od);
PDRectangle pdRect = new PDRectangle((float) rect.getMinX(), (float) rect.getMinY(), (float) rect.getWidth(), (float) rect.getHeight());
-
+
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.TYPE, COSName.BACKGROUND);
dict.setItem(COSName.BBOX, pdRect);
-
+
return dict;
}
-
+
private COSDictionary createPaginationArtifact(StructureType type, Box box) {
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.TYPE, COSName.getPDFName("Pagination"));
return dict;
}
-
+
private static class Token {
}
-
+
private static final Token TRUE_TOKEN = new Token();
private static final Token FALSE_TOKEN = new Token();
private static final Token INSIDE_RUNNING = new Token();
private static final Token STARTING_RUNNING = new Token();
private static final Token NESTED_RUNNING = new Token();
-
+
public Token startStructure(StructureType type, Box box) {
- // Check for items that appear on every page (fixed, running, page margins).
+ // Check for items that appear on every page (fixed, running, page margins).
if (type == StructureType.RUNNING) {
// Only mark artifact for first level of running element (we might have
// nested fixed elements).
@@ -1107,14 +1107,14 @@ public Token startStructure(StructureType type, Box box) {
_cs.beginMarkedContent(COSName.ARTIFACT, run);
return STARTING_RUNNING;
}
-
+
_runningLevel++;
return NESTED_RUNNING;
} else if (_runningLevel > 0) {
// We are in a running artifact.
return INSIDE_RUNNING;
}
-
+
switch (type) {
case LAYER:
case FLOAT:
@@ -1139,12 +1139,12 @@ public Token startStructure(StructureType type, Box box) {
case LIST_MARKER: {
if (box instanceof BlockBox) {
MarkerData markers = ((BlockBox) box).getMarkerData();
-
- if (markers == null ||
+
+ if (markers == null ||
(markers.getGlyphMarker() == null &&
markers.getTextMarker() == null &&
markers.getImageMarker() == null)) {
- return FALSE_TOKEN;
+ return FALSE_TOKEN;
}
}
@@ -1163,9 +1163,9 @@ public Token startStructure(StructureType type, Box box) {
struct = createStructureItem(type, box);
setupStructureElement(struct, box);
}
-
+
FigureContentItem current = createFigureContentStructureItem(type, box);
-
+
if (current != null) {
_cs.beginMarkedContent(COSName.getPDFName(StandardStructureTypes.Figure), current.dict);
return TRUE_TOKEN;
@@ -1186,7 +1186,7 @@ public Token startStructure(StructureType type, Box box) {
public void endStructure(Object token) {
Token value = (Token) token;
-
+
if (value == TRUE_TOKEN) {
_cs.endMarkedContent();
} else if (value == FALSE_TOKEN ||
@@ -1210,11 +1210,11 @@ public void startPage(PDPage page, PdfContentStreamAdapter cs, RenderingContext
this._pageItems = new PageItems();
this._pageItemsMap.put(page, this._pageItems);
}
-
+
public void endPage() {
-
+
}
-
+
private static class AnnotationWithStructureParent {
PDStructureElement structureParent;
PDAnnotation annotation;
@@ -1226,13 +1226,13 @@ public void addLink(Box anchor, Box target, PDAnnotation pdAnnotation, PDPage pa
// We have to append the link annotationobject reference as a kid of its associated structure element.
PDObjectReference ref = new PDObjectReference();
ref.setReferencedObject(pdAnnotation);
- struct.appendKid(ref);
-
+ struct.appendKid(ref);
+
// We also need to save the pair so we can add it to the number tree for reverse lookup.
AnnotationWithStructureParent annotStructParentPair = new AnnotationWithStructureParent();
annotStructParentPair.annotation = pdAnnotation;
annotStructParentPair.structureParent = struct;
-
+
_pageItems._pageAnnotations.add(annotStructParentPair);
}
}
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java
index 70fd065e9..744e53da9 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java
@@ -9,6 +9,7 @@
import com.openhtmltopdf.pdfboxout.quads.Triangle;
import com.openhtmltopdf.render.BlockBox;
import com.openhtmltopdf.render.Box;
+import com.openhtmltopdf.render.PageBox;
import com.openhtmltopdf.render.RenderingContext;
import com.openhtmltopdf.render.displaylist.PagedBoxCollector;
import com.openhtmltopdf.util.LogMessageId;
@@ -46,12 +47,12 @@
public class PdfBoxFastLinkManager {
- private final Map> _linkTargetAreas;
- private final SharedContext _sharedContext;
- private final float _dotsPerPoint;
- private final Box _root;
- private final PdfBoxFastOutputDevice _od;
- private final List _links;
+ private final Map> _linkTargetAreas;
+ private final SharedContext _sharedContext;
+ private final float _dotsPerPoint;
+ private final Box _root;
+ private final PdfBoxFastOutputDevice _od;
+ private final List _links;
private PdfBoxAccessibilityHelper _pdfUa;
/**
@@ -66,198 +67,198 @@ public class PdfBoxFastLinkManager {
*/
private PDAppearanceDictionary _embeddedFileAppearance;
- public PdfBoxFastLinkManager(SharedContext ctx, float dotsPerPoint, Box root, PdfBoxFastOutputDevice od) {
- this._sharedContext = ctx;
- this._dotsPerPoint = dotsPerPoint;
- this._root = root;
- this._od = od;
- this._linkTargetAreas = new HashMap<>();
- this._links = new ArrayList<>();
+ public PdfBoxFastLinkManager(SharedContext ctx, float dotsPerPoint, Box root, PdfBoxFastOutputDevice od) {
+ this._sharedContext = ctx;
+ this._dotsPerPoint = dotsPerPoint;
+ this._root = root;
+ this._od = od;
+ this._linkTargetAreas = new HashMap<>();
+ this._links = new ArrayList<>();
this._embeddedFiles = new HashMap<>();
- }
-
- private Rectangle2D calcTotalLinkArea(RenderingContext c, Box box, float pageHeight, AffineTransform transform) {
- if (_pdfUa != null) {
- // For PDF/UA we need one link annotation per box.
- return createTargetArea(c, box, pageHeight, transform, _root, _od);
- }
-
- Box current = box;
- while (true) {
- Box prev = current.getPreviousSibling();
- if (prev == null || prev.getElement() != box.getElement()) {
- break;
- }
-
- current = prev;
- }
-
- Rectangle2D result = createTargetArea(c, current, pageHeight, transform, _root, _od);
-
- current = current.getNextSibling();
- while (current != null && current.getElement() == box.getElement()) {
- result = add(result, createTargetArea(c, current, pageHeight, transform, _root, _od));
-
- current = current.getNextSibling();
- }
-
- return result;
- }
-
- private Rectangle2D add(Rectangle2D r1, Rectangle2D r2) {
- return r1.createUnion(r2);
- }
-
- private String createRectKey(Rectangle2D rect, Shape linkShape, AffineTransform transform) {
- StringBuilder key = new StringBuilder(
- rect.getMinX() + ":" + rect.getMaxY() + ":" + rect.getMaxX() + ":" + rect.getMinY());
- if (linkShape != null) {
- PathIterator pathIterator = linkShape.getPathIterator(transform);
- double[] vals = new double[6];
- while (!pathIterator.isDone()) {
- int type = pathIterator.currentSegment(vals);
- switch (type) {
- case PathIterator.SEG_CUBICTO:
- key.append("C");
- key.append(vals[0]).append(":").append(vals[1]).append(":").append(vals[2]).append(":")
- .append(vals[3]).append(":").append(vals[4]).append(":").append(vals[5]);
- break;
- case PathIterator.SEG_LINETO:
- key.append("L");
- key.append(vals[0]).append(":").append(vals[1]).append(":");
- break;
- case PathIterator.SEG_MOVETO:
- key.append("M");
- key.append(vals[0]).append(":").append(vals[1]).append(":");
- break;
- case PathIterator.SEG_QUADTO:
- key.append("Q");
- key.append(vals[0]).append(":").append(vals[1]).append(":").append(vals[2]).append(":")
- .append(vals[3]);
- break;
- case PathIterator.SEG_CLOSE:
- key.append("cp");
- break;
- default:
- break;
- }
- pathIterator.next();
- }
- }
- return key.toString();
- }
-
- private Rectangle2D checkLinkArea(LinkDetails link, Shape linkShape) {
- Rectangle2D targetArea = calcTotalLinkArea(link.c, link.box, link.pageHeight, link.transform);
- String key = createRectKey(targetArea, linkShape, link.transform);
- Set keys = _linkTargetAreas.get(link.page);
- if (keys == null) {
- keys = new HashSet<>();
- _linkTargetAreas.put(link.page, keys);
- }
- if (keys.contains(key)) {
- return null;
- }
- keys.add(key);
- return targetArea;
- }
-
- private void processLink(LinkDetails linkDetails) {
- Element elem = linkDetails.box.getElement();
- if (elem != null) {
- NamespaceHandler handler = _sharedContext.getNamespaceHandler();
- String uri = handler.getLinkUri(elem);
- if (uri != null) {
- addUriAsLink(linkDetails, elem, handler, uri, null);
- }
- }
- if (linkDetails.box instanceof BlockBox) {
- ReplacedElement element = ((BlockBox) linkDetails.box).getReplacedElement();
- if (element instanceof IPdfBoxElementWithShapedLinks) {
- Map linkMap = ((IPdfBoxElementWithShapedLinks) element).getLinkMap();
- if (linkMap != null) {
- for (Entry shapeStringEntry : linkMap.entrySet()) {
- Shape shape = shapeStringEntry.getKey();
- String shapeUri = shapeStringEntry.getValue();
- NamespaceHandler handler = _sharedContext.getNamespaceHandler();
- addUriAsLink(linkDetails, elem, handler, shapeUri, shape);
- }
- }
- }
- }
- }
-
- private static boolean isPointEqual(Point2D.Float p1, Point2D.Float p2) {
- final double epsilon = 0.000001;
- return Math.abs(p1.x - p2.x) < epsilon && Math.abs(p1.y - p2.y) < epsilon;
- }
-
- private static void removeDoublicatePoints(List points) {
- boolean rerun;
- do {
- rerun = false;
- /*
- * We can only form triangles if three points are not the same. So we must
- * filter out all points which follow each other and are the same.
- */
- for (int i = 0; i < points.size() - 1; i++) {
- Point2D.Float p1 = points.get(i);
- Point2D.Float p2 = points.get(i + 1);
- if (isPointEqual(p1, p2)) {
- points.remove(i);
- rerun = true;
- }
- }
- /*
- * And we must filter out the same points with gap of 1 between them
- */
- for (int i = 0; i < points.size() - 2; i++) {
- Point2D.Float p1 = points.get(i);
- Point2D.Float p2 = points.get(i + 2);
- if (isPointEqual(p1, p2)) {
- points.remove(i);
- rerun = true;
- }
- }
- } while (rerun);
- }
-
- private void addUriAsLink(LinkDetails linkDetails,
- Element elem, NamespaceHandler handler, String uri, Shape linkShape) {
- if (uri.length() > 1 && uri.charAt(0) == '#') {
- String anchor = uri.substring(1);
- Box target = _sharedContext.getBoxById(anchor);
- if (target != null) {
- PDPageXYZDestination dest = createDestination(linkDetails.c, target);
-
- PDAction action;
- if (handler.getAttributeValue(elem, "onclick") != null
- && !"".equals(handler.getAttributeValue(elem, "onclick"))) {
- action = new PDActionJavaScript(handler.getAttributeValue(elem, "onclick"));
- } else {
- PDActionGoTo go = new PDActionGoTo();
- go.setDestination(dest);
- action = go;
- }
-
- Rectangle2D targetArea = checkLinkArea(linkDetails, linkShape);
- if (targetArea == null) {
- return;
- }
-
- PDAnnotationLink annot = new PDAnnotationLink();
- annot.setAction(action);
+ }
+
+ private Rectangle2D calcTotalLinkArea(RenderingContext c, Box box, float pageHeight, AffineTransform transform) {
+ if (_pdfUa != null) {
+ // For PDF/UA we need one link annotation per box.
+ return createTargetArea(c, box, pageHeight, transform, c.getPage(), _od);
+ }
+
+ Box current = box;
+ while (true) {
+ Box prev = current.getPreviousSibling();
+ if (prev == null || prev.getElement() != box.getElement()) {
+ break;
+ }
+
+ current = prev;
+ }
+
+ Rectangle2D result = createTargetArea(c, current, pageHeight, transform, c.getPage(), _od);
+
+ current = current.getNextSibling();
+ while (current != null && current.getElement() == box.getElement()) {
+ result = add(result, createTargetArea(c, current, pageHeight, transform, c.getPage(), _od));
+
+ current = current.getNextSibling();
+ }
+
+ return result;
+ }
+
+ private Rectangle2D add(Rectangle2D r1, Rectangle2D r2) {
+ return r1.createUnion(r2);
+ }
+
+ private String createRectKey(Rectangle2D rect, Shape linkShape, AffineTransform transform) {
+ StringBuilder key = new StringBuilder(
+ rect.getMinX() + ":" + rect.getMaxY() + ":" + rect.getMaxX() + ":" + rect.getMinY());
+ if (linkShape != null) {
+ PathIterator pathIterator = linkShape.getPathIterator(transform);
+ double[] vals = new double[6];
+ while (!pathIterator.isDone()) {
+ int type = pathIterator.currentSegment(vals);
+ switch (type) {
+ case PathIterator.SEG_CUBICTO:
+ key.append("C");
+ key.append(vals[0]).append(":").append(vals[1]).append(":").append(vals[2]).append(":")
+ .append(vals[3]).append(":").append(vals[4]).append(":").append(vals[5]);
+ break;
+ case PathIterator.SEG_LINETO:
+ key.append("L");
+ key.append(vals[0]).append(":").append(vals[1]).append(":");
+ break;
+ case PathIterator.SEG_MOVETO:
+ key.append("M");
+ key.append(vals[0]).append(":").append(vals[1]).append(":");
+ break;
+ case PathIterator.SEG_QUADTO:
+ key.append("Q");
+ key.append(vals[0]).append(":").append(vals[1]).append(":").append(vals[2]).append(":")
+ .append(vals[3]);
+ break;
+ case PathIterator.SEG_CLOSE:
+ key.append("cp");
+ break;
+ default:
+ break;
+ }
+ pathIterator.next();
+ }
+ }
+ return key.toString();
+ }
+
+ private Rectangle2D checkLinkArea(LinkDetails link, Shape linkShape) {
+ Rectangle2D targetArea = calcTotalLinkArea(link.c, link.box, link.pageHeight, link.transform);
+ String key = createRectKey(targetArea, linkShape, link.transform);
+ Set keys = _linkTargetAreas.get(link.page);
+ if (keys == null) {
+ keys = new HashSet<>();
+ _linkTargetAreas.put(link.page, keys);
+ }
+ if (keys.contains(key)) {
+ return null;
+ }
+ keys.add(key);
+ return targetArea;
+ }
+
+ private void processLink(LinkDetails linkDetails) {
+ Element elem = linkDetails.box.getElement();
+ if (elem != null) {
+ NamespaceHandler handler = _sharedContext.getNamespaceHandler();
+ String uri = handler.getLinkUri(elem);
+ if (uri != null) {
+ addUriAsLink(linkDetails, elem, handler, uri, null);
+ }
+ }
+ if (linkDetails.box instanceof BlockBox) {
+ ReplacedElement element = ((BlockBox) linkDetails.box).getReplacedElement();
+ if (element instanceof IPdfBoxElementWithShapedLinks) {
+ Map linkMap = ((IPdfBoxElementWithShapedLinks) element).getLinkMap();
+ if (linkMap != null) {
+ for (Entry shapeStringEntry : linkMap.entrySet()) {
+ Shape shape = shapeStringEntry.getKey();
+ String shapeUri = shapeStringEntry.getValue();
+ NamespaceHandler handler = _sharedContext.getNamespaceHandler();
+ addUriAsLink(linkDetails, elem, handler, shapeUri, shape);
+ }
+ }
+ }
+ }
+ }
+
+ private static boolean isPointEqual(Point2D.Float p1, Point2D.Float p2) {
+ final double epsilon = 0.000001;
+ return Math.abs(p1.x - p2.x) < epsilon && Math.abs(p1.y - p2.y) < epsilon;
+ }
+
+ private static void removeDoublicatePoints(List points) {
+ boolean rerun;
+ do {
+ rerun = false;
+ /*
+ * We can only form triangles if three points are not the same. So we must
+ * filter out all points which follow each other and are the same.
+ */
+ for (int i = 0; i < points.size() - 1; i++) {
+ Point2D.Float p1 = points.get(i);
+ Point2D.Float p2 = points.get(i + 1);
+ if (isPointEqual(p1, p2)) {
+ points.remove(i);
+ rerun = true;
+ }
+ }
+ /*
+ * And we must filter out the same points with gap of 1 between them
+ */
+ for (int i = 0; i < points.size() - 2; i++) {
+ Point2D.Float p1 = points.get(i);
+ Point2D.Float p2 = points.get(i + 2);
+ if (isPointEqual(p1, p2)) {
+ points.remove(i);
+ rerun = true;
+ }
+ }
+ } while (rerun);
+ }
+
+ private void addUriAsLink(LinkDetails linkDetails,
+ Element elem, NamespaceHandler handler, String uri, Shape linkShape) {
+ if (uri.length() > 1 && uri.charAt(0) == '#') {
+ String anchor = uri.substring(1);
+ Box target = _sharedContext.getBoxById(anchor);
+ if (target != null) {
+ PDPageXYZDestination dest = createDestination(linkDetails.c, target);
+
+ PDAction action;
+ if (handler.getAttributeValue(elem, "onclick") != null
+ && !"".equals(handler.getAttributeValue(elem, "onclick"))) {
+ action = new PDActionJavaScript(handler.getAttributeValue(elem, "onclick"));
+ } else {
+ PDActionGoTo go = new PDActionGoTo();
+ go.setDestination(dest);
+ action = go;
+ }
+
+ Rectangle2D targetArea = checkLinkArea(linkDetails, linkShape);
+ if (targetArea == null) {
+ return;
+ }
+
+ PDAnnotationLink annot = new PDAnnotationLink();
+ annot.setAction(action);
AnnotationContainer annotContainer = new AnnotationContainer.PDAnnotationLinkContainer(annot);
- if (!placeAnnotation(linkDetails.transform, linkShape, targetArea, annotContainer))
- return;
+ if (!placeAnnotation(linkDetails.transform, linkShape, targetArea, annotContainer))
+ return;
addLinkToPage(linkDetails.page, annotContainer, linkDetails.box, target);
- } else {
- XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.GENERAL_PDF_COULD_NOT_FIND_VALID_TARGET_FOR_LINK, uri);
- }
- } else if (isURI(uri)) {
+ } else {
+ XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.GENERAL_PDF_COULD_NOT_FIND_VALID_TARGET_FOR_LINK, uri);
+ }
+ } else if (isURI(uri)) {
AnnotationContainer annotContainer = null;
if (!elem.hasAttribute("download")) {
@@ -289,7 +290,7 @@ private void addUriAsLink(LinkDetails linkDetails,
/**
* Create a file attachment link, being careful not to embed the same
* file (as specified by uri) more than once.
- *
+ *
* The element should have the following attributes:
* download="embedded-filename.ext",
* data-content-type="file-mime-type" which
@@ -315,8 +316,8 @@ private AnnotationContainer createFileEmbedLinkAnnotation(
if (file != null) {
try {
- String contentType = elem.getAttribute("data-content-type").isEmpty() ?
- "application/octet-stream" :
+ String contentType = elem.getAttribute("data-content-type").isEmpty() ?
+ "application/octet-stream" :
elem.getAttribute("data-content-type");
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(_od.getWriter(), new ByteArrayInputStream(file));
@@ -339,8 +340,8 @@ private AnnotationContainer createFileEmbedLinkAnnotation(
// The PDF/A3 standard requires one to specify the relationship
// this embedded file has to the link annotation.
if (elem.hasAttribute("relationship") &&
- Arrays.asList("Source", "Supplement", "Data", "Alternative", "Unspecified")
- .contains(elem.getAttribute("relationship"))) {
+ Arrays.asList("Source", "Supplement", "Data", "Alternative", "Unspecified")
+ .contains(elem.getAttribute("relationship"))) {
fs.getCOSObject().setItem(
COSName.getPDFName("AFRelationship"),
COSName.getPDFName(elem.getAttribute("relationship")));
@@ -392,218 +393,220 @@ private PDAppearanceDictionary createFileEmbedLinkAppearance() {
return appearanceDictionary;
}
- private static boolean isURI(String uri) {
- try {
- return URI.create(uri) != null;
- } catch (IllegalArgumentException e) {
- XRLog.log(Level.INFO, LogMessageId.LogMessageId1Param.GENERAL_PDF_URI_IN_HREF_IS_NOT_A_VALID_URI, uri);
- return false;
- }
- }
-
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- private boolean placeAnnotation(AffineTransform transform, Shape linkShape, Rectangle2D targetArea,
- AnnotationContainer annot) {
- annot.setRectangle(new PDRectangle((float) targetArea.getMinX(), (float) targetArea.getMinY(),
- (float) targetArea.getWidth(), (float) targetArea.getHeight()));
-
- // PDF/A standard requires the print flag to be set and there shouldn't
- // be any harm in setting it for other documents.
- annot.setPrinted(true);
-
- if (linkShape != null) {
- QuadPointShape quadPointsResult = mapShapeToQuadPoints(transform, linkShape, targetArea);
- /*
- * Is this not an area shape? Then we can not setup quads - ignore this shape.
- */
- if (quadPointsResult.quadPoints.length == 0)
- return false;
- annot.setQuadPoints(quadPointsResult.quadPoints);
- Rectangle2D reducedTarget = quadPointsResult.boundingBox;
- annot.setRectangle(new PDRectangle((float) reducedTarget.getMinX(), (float) reducedTarget.getMinY(),
- (float) reducedTarget.getWidth(), (float) reducedTarget.getHeight()));
- }
- return true;
- }
-
- static class QuadPointShape {
- float[] quadPoints;
- Rectangle2D boundingBox;
- }
-
- static QuadPointShape mapShapeToQuadPoints(AffineTransform transform, Shape linkShape, Rectangle2D targetArea) {
- List points = new ArrayList<>();
- AffineTransform transformForQuads = new AffineTransform();
- transformForQuads.translate(targetArea.getMinX(), targetArea.getMinY());
- // We must flip the whole thing upside down
- transformForQuads.translate(0, targetArea.getHeight());
- transformForQuads.scale(1, -1);
- transformForQuads.concatenate(AffineTransform.getScaleInstance(transform.getScaleX(), transform.getScaleX()));
- Area area = new Area(linkShape);
- PathIterator pathIterator = area.getPathIterator(transformForQuads, 1.0);
- double[] vals = new double[6];
- while (!pathIterator.isDone()) {
- int type = pathIterator.currentSegment(vals);
- switch (type) {
- case PathIterator.SEG_CUBICTO:
- throw new RuntimeException("Invalid State, Area should never give us a curve here!");
- case PathIterator.SEG_LINETO:
- points.add(new Point2D.Float((float) vals[0], (float) vals[1]));
- break;
- case PathIterator.SEG_MOVETO:
- points.add(new Point2D.Float((float) vals[0], (float) vals[1]));
- break;
- case PathIterator.SEG_QUADTO:
- throw new RuntimeException("Invalid State, Area should never give us a curve here!");
- case PathIterator.SEG_CLOSE:
- break;
- default:
- break;
- }
- pathIterator.next();
- }
-
- removeDoublicatePoints(points);
-
- KongAlgo algo = new KongAlgo(points);
- algo.runKong();
-
- float minX = (float) targetArea.getMaxX();
- float maxX = (float) targetArea.getMinX();
- float minY = (float) targetArea.getMaxY();
- float maxY = (float) targetArea.getMinY();
-
- float[] ret = new float[algo.getTriangles().size() * 8];
- int i = 0;
- for (Triangle triangle : algo.getTriangles()) {
- ret[i++] = triangle.a.x;
- ret[i++] = triangle.a.y;
- ret[i++] = triangle.b.x;
- ret[i++] = triangle.b.y;
- /*
- * To get a quad we add the point between b and c
- */
- ret[i++] = triangle.b.x + (triangle.c.x - triangle.b.x) / 2;
- ret[i++] = triangle.b.y + (triangle.c.y - triangle.b.y) / 2;
-
- ret[i++] = triangle.c.x;
- ret[i++] = triangle.c.y;
-
- for (Point2D.Float p : new Point2D.Float[] { triangle.a, triangle.b, triangle.c }) {
- float x = p.x;
- float y = p.y;
-
- minX = Math.min(x, minX);
- minY = Math.min(y, minY);
-
- maxX = Math.max(x, maxX);
- maxY = Math.max(y, maxY);
- }
- }
-
- //noinspection ConstantConditions
- if (ret.length % 8 != 0)
- throw new IllegalStateException("Not exact 8xn QuadPoints!");
- for (; i < ret.length; i += 2) {
- if (ret[i] < targetArea.getMinX() || ret[i] > targetArea.getMaxX())
- throw new IllegalStateException("Invalid rectangle calculation. Map shape is out of bound.");
- if (ret[i + 1] < targetArea.getMinY() || ret[
- i + 1] > targetArea.getMaxY())
- throw new IllegalStateException("Invalid rectangle calculation. Map shape is out of bound.");
- }
-
- QuadPointShape result = new QuadPointShape();
- result.quadPoints = ret;
- Rectangle2D.Float boundingRectangle = new Rectangle2D.Float(minX, minY, maxX - minX, maxY - minY);
- Rectangle2D.intersect(targetArea, boundingRectangle, boundingRectangle);
- result.boundingBox = boundingRectangle;
- return result;
- }
-
- private void addLinkToPage(
- PDPage page, AnnotationContainer annot, Box anchor, Box target) {
- PDBorderStyleDictionary styleDict = new PDBorderStyleDictionary();
- styleDict.setWidth(0);
- styleDict.setStyle(PDBorderStyleDictionary.STYLE_SOLID);
- annot.setBorderStyle(styleDict);
-
- try {
- List annots = page.getAnnotations();
-
- if (annots == null) {
- annots = new ArrayList<>();
- page.setAnnotations(annots);
- }
-
- annots.add(annot.getPdAnnotation());
-
- if (_pdfUa != null) {
- _pdfUa.addLink(anchor, target, annot.getPdAnnotation(), page);
- }
- } catch (IOException e) {
- throw new PdfContentStreamAdapter.PdfException("processLink", e);
- }
- }
-
- private PDPageXYZDestination createDestination(RenderingContext c, Box box) {
- return PdfBoxBookmarkManager.createBoxDestination(c, _od.getWriter(), _od, _dotsPerPoint, _root, box);
- }
-
- public static Rectangle2D createTargetArea(RenderingContext c, Box box, float pageHeight, AffineTransform transform,
- Box _root, PdfBoxOutputDevice _od) {
- Rectangle bounds = PagedBoxCollector.findAdjustedBoundsForContentBox(c, box);
-
- if (!c.isInPageMargins()) {
- int shadow = c.getShadowPageNumber();
- Rectangle pageBounds = shadow == -1 ?
- c.getPage().getDocumentCoordinatesContentBounds(c) :
- c.getPage().getDocumentCoordinatesContentBoundsForInsertedPage(c, shadow);
-
- bounds = bounds.intersection(pageBounds);
- }
-
- Point2D pt = new Point2D.Float(bounds.x, (float) bounds.getMaxY());
- Point2D ptTransformed = transform.transform(pt, null);
-
- return new Rectangle2D.Float((float) ptTransformed.getX(),
- _od.normalizeY((float) ptTransformed.getY(), pageHeight),
- _od.getDeviceLength(bounds.width),
- _od.getDeviceLength(bounds.height));
- }
-
- private static class LinkDetails {
-
- RenderingContext c;
- Box box;
- Rectangle2D targetArea;
- PDPage page;
- float pageHeight;
- AffineTransform transform;
- }
-
- public void processLinkLater(RenderingContext c, Box box, PDPage page, float pageHeight,
- AffineTransform transform) {
-
- if ((box instanceof BlockBox &&
- ((BlockBox) box).getReplacedElement() != null) ||
- (box.getElement() != null && box.getElement().getNodeName().equals("a"))) {
-
- LinkDetails link = new LinkDetails();
- link.c = (RenderingContext) c.clone();
- link.box = box;
- link.page = page;
- link.pageHeight = pageHeight;
- link.transform = (AffineTransform) transform.clone();
- link.targetArea = calcTotalLinkArea(c, box, pageHeight, transform);
-
- _links.add(link);
- }
- }
-
- public void processLinks(PdfBoxAccessibilityHelper pdfUa) {
- this._pdfUa = pdfUa;
- for (LinkDetails link : _links) {
- processLink(link);
- }
- }
+ private static boolean isURI(String uri) {
+ try {
+ return URI.create(uri) != null;
+ } catch (IllegalArgumentException e) {
+ XRLog.log(Level.INFO, LogMessageId.LogMessageId1Param.GENERAL_PDF_URI_IN_HREF_IS_NOT_A_VALID_URI, uri);
+ return false;
+ }
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean placeAnnotation(AffineTransform transform, Shape linkShape, Rectangle2D targetArea,
+ AnnotationContainer annot) {
+ annot.setRectangle(new PDRectangle((float) targetArea.getMinX(), (float) targetArea.getMinY(),
+ (float) targetArea.getWidth(), (float) targetArea.getHeight()));
+
+ // PDF/A standard requires the print flag to be set and there shouldn't
+ // be any harm in setting it for other documents.
+ annot.setPrinted(true);
+
+ if (linkShape != null) {
+ QuadPointShape quadPointsResult = mapShapeToQuadPoints(transform, linkShape, targetArea);
+ /*
+ * Is this not an area shape? Then we can not setup quads - ignore this shape.
+ */
+ if (quadPointsResult.quadPoints.length == 0)
+ return false;
+ annot.setQuadPoints(quadPointsResult.quadPoints);
+ Rectangle2D reducedTarget = quadPointsResult.boundingBox;
+ annot.setRectangle(new PDRectangle((float) reducedTarget.getMinX(), (float) reducedTarget.getMinY(),
+ (float) reducedTarget.getWidth(), (float) reducedTarget.getHeight()));
+ }
+ return true;
+ }
+
+ static class QuadPointShape {
+ float[] quadPoints;
+ Rectangle2D boundingBox;
+ }
+
+ static QuadPointShape mapShapeToQuadPoints(AffineTransform transform, Shape linkShape, Rectangle2D targetArea) {
+ List points = new ArrayList<>();
+ AffineTransform transformForQuads = new AffineTransform();
+ transformForQuads.translate(targetArea.getMinX(), targetArea.getMinY());
+ // We must flip the whole thing upside down
+ transformForQuads.translate(0, targetArea.getHeight());
+ transformForQuads.scale(1, -1);
+ transformForQuads.concatenate(AffineTransform.getScaleInstance(transform.getScaleX(), transform.getScaleX()));
+ Area area = new Area(linkShape);
+ PathIterator pathIterator = area.getPathIterator(transformForQuads, 1.0);
+ double[] vals = new double[6];
+ while (!pathIterator.isDone()) {
+ int type = pathIterator.currentSegment(vals);
+ switch (type) {
+ case PathIterator.SEG_CUBICTO:
+ throw new RuntimeException("Invalid State, Area should never give us a curve here!");
+ case PathIterator.SEG_LINETO:
+ points.add(new Point2D.Float((float) vals[0], (float) vals[1]));
+ break;
+ case PathIterator.SEG_MOVETO:
+ points.add(new Point2D.Float((float) vals[0], (float) vals[1]));
+ break;
+ case PathIterator.SEG_QUADTO:
+ throw new RuntimeException("Invalid State, Area should never give us a curve here!");
+ case PathIterator.SEG_CLOSE:
+ break;
+ default:
+ break;
+ }
+ pathIterator.next();
+ }
+
+ removeDoublicatePoints(points);
+
+ KongAlgo algo = new KongAlgo(points);
+ algo.runKong();
+
+ float minX = (float) targetArea.getMaxX();
+ float maxX = (float) targetArea.getMinX();
+ float minY = (float) targetArea.getMaxY();
+ float maxY = (float) targetArea.getMinY();
+
+ float[] ret = new float[algo.getTriangles().size() * 8];
+ int i = 0;
+ for (Triangle triangle : algo.getTriangles()) {
+ ret[i++] = triangle.a.x;
+ ret[i++] = triangle.a.y;
+ ret[i++] = triangle.b.x;
+ ret[i++] = triangle.b.y;
+ /*
+ * To get a quad we add the point between b and c
+ */
+ ret[i++] = triangle.b.x + (triangle.c.x - triangle.b.x) / 2;
+ ret[i++] = triangle.b.y + (triangle.c.y - triangle.b.y) / 2;
+
+ ret[i++] = triangle.c.x;
+ ret[i++] = triangle.c.y;
+
+ for (Point2D.Float p : new Point2D.Float[]{triangle.a, triangle.b, triangle.c}) {
+ float x = p.x;
+ float y = p.y;
+
+ minX = Math.min(x, minX);
+ minY = Math.min(y, minY);
+
+ maxX = Math.max(x, maxX);
+ maxY = Math.max(y, maxY);
+ }
+ }
+
+ //noinspection ConstantConditions
+ if (ret.length % 8 != 0)
+ throw new IllegalStateException("Not exact 8xn QuadPoints!");
+ for (; i < ret.length; i += 2) {
+ if (ret[i] < targetArea.getMinX() || ret[i] > targetArea.getMaxX())
+ throw new IllegalStateException("Invalid rectangle calculation. Map shape is out of bound.");
+ if (ret[i + 1] < targetArea.getMinY() || ret[
+ i + 1] > targetArea.getMaxY())
+ throw new IllegalStateException("Invalid rectangle calculation. Map shape is out of bound.");
+ }
+
+ QuadPointShape result = new QuadPointShape();
+ result.quadPoints = ret;
+ Rectangle2D.Float boundingRectangle = new Rectangle2D.Float(minX, minY, maxX - minX, maxY - minY);
+ Rectangle2D.intersect(targetArea, boundingRectangle, boundingRectangle);
+ result.boundingBox = boundingRectangle;
+ return result;
+ }
+
+ private void addLinkToPage(
+ PDPage page, AnnotationContainer annot, Box anchor, Box target) {
+ PDBorderStyleDictionary styleDict = new PDBorderStyleDictionary();
+ styleDict.setWidth(0);
+ styleDict.setStyle(PDBorderStyleDictionary.STYLE_SOLID);
+ annot.setBorderStyle(styleDict);
+
+ try {
+ List annots = page.getAnnotations();
+
+ if (annots == null) {
+ annots = new ArrayList<>();
+ page.setAnnotations(annots);
+ }
+
+ annots.add(annot.getPdAnnotation());
+
+ if (_pdfUa != null) {
+ _pdfUa.addLink(anchor, target, annot.getPdAnnotation(), page);
+ }
+ } catch (IOException e) {
+ throw new PdfContentStreamAdapter.PdfException("processLink", e);
+ }
+ }
+
+ private PDPageXYZDestination createDestination(RenderingContext c, Box box) {
+ return PdfBoxBookmarkManager.createBoxDestination(c, _od.getWriter(), _od, _dotsPerPoint, _root, box);
+ }
+
+
+ public static Rectangle2D createTargetArea(RenderingContext c, Box box, float pageHeight, AffineTransform transform,
+ PageBox pageBox, PdfBoxOutputDevice _od) {
+ Rectangle bounds = PagedBoxCollector.findAdjustedBoundsForContentBox(c, box);
+
+ if (!c.isInPageMargins()) {
+ int shadow = c.getShadowPageNumber();
+
+ Rectangle pageBounds = shadow == -1 ?
+ pageBox.getDocumentCoordinatesContentBounds(c) :
+ pageBox.getDocumentCoordinatesContentBoundsForInsertedPage(c, shadow);
+
+ bounds = bounds.intersection(pageBounds);
+ }
+
+ Point2D pt = new Point2D.Float(bounds.x, (float) bounds.getMaxY());
+ Point2D ptTransformed = transform.transform(pt, null);
+
+ return new Rectangle2D.Float((float) ptTransformed.getX(),
+ _od.normalizeY((float) ptTransformed.getY(), pageHeight),
+ _od.getDeviceLength(bounds.width),
+ _od.getDeviceLength(bounds.height));
+ }
+
+ private static class LinkDetails {
+
+ RenderingContext c;
+ Box box;
+ Rectangle2D targetArea;
+ PDPage page;
+ float pageHeight;
+ AffineTransform transform;
+ }
+
+ public void processLinkLater(RenderingContext c, Box box, PDPage page, float pageHeight,
+ AffineTransform transform) {
+
+ if ((box instanceof BlockBox &&
+ ((BlockBox) box).getReplacedElement() != null) ||
+ (box.getElement() != null && box.getElement().getNodeName().equals("a"))) {
+
+ LinkDetails link = new LinkDetails();
+ link.c = (RenderingContext) c.clone();
+ link.box = box;
+ link.page = page;
+ link.pageHeight = pageHeight;
+ link.transform = (AffineTransform) transform.clone();
+ link.targetArea = calcTotalLinkArea(c, box, pageHeight, transform);
+
+ _links.add(link);
+ }
+ }
+
+ public void processLinks(PdfBoxAccessibilityHelper pdfUa) {
+ this._pdfUa = pdfUa;
+ for (LinkDetails link : _links) {
+ processLink(link);
+ }
+ }
}
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java
index 61f144704..d598e2672 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java
@@ -293,8 +293,8 @@ public void paintBackground(RenderingContext c, Box box) {
}
}
- private void processControls() {
- _formState.processControls(_sharedContext, _writer, _root);
+ private void processControls(RenderingContext renderingContext) {
+ _formState.processControls(_sharedContext, _writer, _root, renderingContext);
}
/**
@@ -898,7 +898,7 @@ public void finish(RenderingContext c, Box root) {
_bmManager.writeOutline(c, root);
// Also need access to the structure tree.
- processControls();
+ processControls(c);
_linkManager.processLinks(_pdfUa);
if (_pdfUa != null) {
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxForm.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxForm.java
index 5f359fb3f..4dc90c8a2 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxForm.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxForm.java
@@ -16,6 +16,7 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import com.openhtmltopdf.render.PageBox;
import com.openhtmltopdf.util.LogMessageId;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
@@ -41,6 +42,7 @@
import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDPushButton;
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
+import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
import org.w3c.dom.Element;
@@ -61,13 +63,13 @@
public class PdfBoxForm {
// The global(per document) form state container
private final PdfBoxPerDocumentFormState docFormsStateContainer;
-
+
// The output device
private final PdfBoxOutputDevice od;
-
+
// The form element itself.
private final Element element;
-
+
// All controls, with a font name if needed.
private final List controls = new ArrayList<>();
@@ -76,7 +78,7 @@ public class PdfBoxForm {
// We've got to find all the radio button controls that belong to a group (common name).
private final Map> radioGroups = new LinkedHashMap<>();
-
+
// Contains a tree of fields in the form:
// person
// person.details
@@ -84,7 +86,7 @@ public class PdfBoxForm {
// person.details.phone
// etc.
private final Map allFieldMap = new HashMap<>();
-
+
// A link in the tree of fields. We have this so that each field can look up
// its parent field.
private static class Field {
@@ -95,39 +97,41 @@ private static class Field {
// set its kids appropriately.
private boolean isTerminal;
}
-
+
public static class Control {
public final Box box;
private final PDPage page;
+ private final PageBox pageBox;
private final AffineTransform transform;
private final RenderingContext c;
private final float pageHeight;
-
+
public Control(Box box, PDPage page, AffineTransform transform, RenderingContext c, float pageHeight) {
this.box = box;
this.page = page;
this.transform = transform;
this.c = c;
this.pageHeight = pageHeight;
+ this.pageBox = c.getPage();
}
}
-
+
private static class ControlFontPair {
private final String fontName;
private final Control control;
-
+
private ControlFontPair(Control control, String fontName) {
this.control = control;
this.fontName = fontName;
}
}
-
+
private PdfBoxForm(Element element, PdfBoxPerDocumentFormState forms, PdfBoxOutputDevice od) {
this.element = element;
this.od = od;
this.docFormsStateContainer = forms;
}
-
+
public static PdfBoxForm createForm(Element e, PdfBoxPerDocumentFormState forms, PdfBoxOutputDevice od) {
return new PdfBoxForm(e, forms, od);
}
@@ -135,8 +139,8 @@ public static PdfBoxForm createForm(Element e, PdfBoxPerDocumentFormState forms,
public void addControl(Control ctrl, String fontName) {
controls.add(new ControlFontPair(ctrl, fontName));
}
-
- /**
+
+ /**
* This method will create a tree of names, both non-terminal
* and terminal.
*/
@@ -145,9 +149,9 @@ private void processControlNames() {
if (!control.control.box.getElement().hasAttribute("name")) {
continue;
}
-
+
String name = control.control.box.getElement().getAttribute("name");
-
+
if (!name.contains(".")) {
// It's a root field!
Field f = new Field();
@@ -157,12 +161,12 @@ private void processControlNames() {
allFieldMap.put(name, f);
} else {
String[] partials = name.split(Pattern.quote("."));
-
+
for (int i = 1; i <= partials.length; i++) {
// Given a field name such as person.details.name
// we check that 'person' is created first, then 'person.details' and
// finally 'person.details.name'.
-
+
String[] parent = new String[i];
System.arraycopy(partials, 0, parent, 0, i);
String parentQualifiedName = ArrayUtil.join(parent, ".");
@@ -171,7 +175,7 @@ private void processControlNames() {
if (f == null) {
Field fCreated = new Field();
fCreated.qualifiedName = parentQualifiedName;
- fCreated.partialName = parent[i - 1];
+ fCreated.partialName = parent[i - 1];
fCreated.isTerminal = (i == partials.length);
allFieldMap.put(parentQualifiedName, fCreated);
}
@@ -179,92 +183,92 @@ private void processControlNames() {
}
}
}
-
+
/**
* This method will create the non terminal fields.
* It is called recursively to create all non-terminal field descendants.
* It should be called after all the PDField objects are created.
*/
private void createNonTerminalFields(Field f, PDAcroForm form) {
- if (!f.isTerminal) {
- COSArray kids = new COSArray();
+ if (!f.isTerminal) {
+ COSArray kids = new COSArray();
- for (Field f2 : allFieldMap.values()) {
- if (f2.qualifiedName.indexOf(f.qualifiedName) == 0 && // Its a descendant or identical.
+ for (Field f2 : allFieldMap.values()) {
+ if (f2.qualifiedName.indexOf(f.qualifiedName) == 0 && // Its a descendant or identical.
f2.qualifiedName.length() > f.qualifiedName.length() + 1 && // Its not identical.
!f2.qualifiedName.substring(f.qualifiedName.length() + 1).contains(".")) { // Its a direct child.
- kids.add(f2.field.getCOSObject());
- f2.field.getCOSObject().setItem(COSName.PARENT, f.field.getCOSObject());
- createNonTerminalFields(f2, form);
- }
+ kids.add(f2.field.getCOSObject());
+ f2.field.getCOSObject().setItem(COSName.PARENT, f.field.getCOSObject());
+ createNonTerminalFields(f2, form);
}
-
+ }
+
- f.field.getCOSObject().setItem(COSName.KIDS, kids);
+ f.field.getCOSObject().setItem(COSName.KIDS, kids);
+ }
+ }
+
+ /**
+ * Calls createNonTerminalFields on all root non-terminal fields.
+ * Otherwise, root fields are added to the acro form field collection.
+ */
+ private void createNonTerminalFields(PDAcroForm form) {
+ for (Field f : allFieldMap.values()) {
+ if (!f.isTerminal) {
+ PDNonTerminalField nonTerminal = new PDNonTerminalField(form);
+ nonTerminal.setPartialName(f.partialName);
+ f.field = nonTerminal;
+ }
+ }
+
+ for (Field f : allFieldMap.values()) {
+ if (!f.qualifiedName.contains(".")) {
+ createNonTerminalFields(f, form);
+ form.getFields().add(f.field);
}
- }
-
- /**
- * Calls createNonTerminalFields on all root non-terminal fields.
- * Otherwise, root fields are added to the acro form field collection.
- */
- private void createNonTerminalFields(PDAcroForm form) {
- for (Field f : allFieldMap.values()) {
- if (!f.isTerminal) {
- PDNonTerminalField nonTerminal = new PDNonTerminalField(form);
- nonTerminal.setPartialName(f.partialName);
- f.field = nonTerminal;
- }
- }
-
- for (Field f : allFieldMap.values()) {
- if (!f.qualifiedName.contains(".")) {
- createNonTerminalFields(f, form);
- form.getFields().add(f.field);
- }
- }
- }
-
+ }
+ }
+
/**
* Get a PDF graphics operator for a specific color.
*/
private static String getColorOperator(FSColor color) {
String colorOperator = "";
-
+
if (color instanceof FSRGBColor) {
FSRGBColor rgb = (FSRGBColor) color;
float r = (float) rgb.getRed() / 255;
float g = (float) rgb.getGreen() / 255;
float b = (float) rgb.getBlue() / 255;
-
+
colorOperator =
- String.format(Locale.US, "%.4f", r) + ' ' +
- String.format(Locale.US, "%.4f", g) + ' ' +
- String.format(Locale.US, "%.4f", b) + ' ' +
- "rg";
+ String.format(Locale.US, "%.4f", r) + ' ' +
+ String.format(Locale.US, "%.4f", g) + ' ' +
+ String.format(Locale.US, "%.4f", b) + ' ' +
+ "rg";
} else if (color instanceof FSCMYKColor) {
FSCMYKColor cmyk = (FSCMYKColor) color;
float c = cmyk.getCyan();
float m = cmyk.getMagenta();
float y = cmyk.getYellow();
float k = cmyk.getBlack();
-
- colorOperator =
+
+ colorOperator =
String.format(Locale.US, "%.4f", c) + ' ' +
- String.format(Locale.US, "%.4f", m) + ' ' +
- String.format(Locale.US, "%.4f", y) + ' ' +
- String.format(Locale.US, "%.4f", k) + ' ' +
- "k";
+ String.format(Locale.US, "%.4f", m) + ' ' +
+ String.format(Locale.US, "%.4f", y) + ' ' +
+ String.format(Locale.US, "%.4f", k) + ' ' +
+ "k";
}
-
+
return colorOperator;
}
-
+
private String getTextareaText(Element e) {
return DOMUtil.getText(e);
}
-
+
private String populateOptions(Element e, List labels, List values, List selectedIndices) {
List opts = DOMUtil.getChildren(e, "option");
if (opts == null) {
@@ -273,141 +277,147 @@ private String populateOptions(Element e, List labels, List valu
}
String selected = "";
int i = 0;
-
+
for (Element opt : opts) {
String label = DOMUtil.getText(opt);
labels.add(label);
-
+
if (opt.hasAttribute("value")) {
values.add(opt.getAttribute("value"));
} else {
values.add(label);
}
-
+
if (selected.isEmpty()) {
selected = label;
}
-
+
if (opt.hasAttribute("selected")) {
selected = label;
}
-
+
if (opt.hasAttribute("selected") && selectedIndices != null) {
selectedIndices.add(i);
}
i++;
}
-
+
return selected;
}
-
+
private void processMultiSelectControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root) throws IOException {
PDListBox field = new PDListBox(acro);
setPartialNameToField(ctrl, field);
field.setMultiSelect(true);
-
+
List labels = new ArrayList<>();
List values = new ArrayList<>();
List selected = new ArrayList<>();
populateOptions(ctrl.box.getElement(), labels, values, selected);
-
+
field.setOptions(values, labels);
field.setSelectedOptionsIndex(selected);
-
+
FSColor color = ctrl.box.getStyle().getColor();
String colorOperator = getColorOperator(color);
-
+
String fontInstruction = "/" + pair.fontName + " 0 Tf";
field.setDefaultAppearance(fontInstruction + ' ' + colorOperator);
-
+
if (ctrl.box.getElement().hasAttribute("required")) {
field.setRequired(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("readonly")) {
field.setReadOnly(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("title")) {
field.setAlternateFieldName(ctrl.box.getElement().getAttribute("title"));
}
-
+
PDAnnotationWidget widget = field.getWidgets().get(0);
- Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform, root, od);
- PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(), (float) rect2D.getWidth(), (float) rect2D.getHeight());
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform,
+ ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
widget.setRectangle(rect);
widget.setPage(ctrl.page);
widget.setPrinted(true);
-
+
ctrl.page.getAnnotations().add(widget);
}
-
+
/**
* Processes select controls and the custom openhtmltopdf-combo control.
*/
- private void processSelectControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root) throws IOException {
+ private void processSelectControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root)
+ throws IOException {
PDComboBox field = new PDComboBox(acro);
setPartialNameToField(ctrl, field);
-
+
List labels = new ArrayList<>();
List values = new ArrayList<>();
String selectedLabel = populateOptions(ctrl.box.getElement(), labels, values, null);
-
+
field.setOptions(values, labels);
field.setValue(selectedLabel);
field.setDefaultValue(selectedLabel);
-
+
FSColor color = ctrl.box.getStyle().getColor();
String colorOperator = getColorOperator(color);
-
+
String fontInstruction = "/" + pair.fontName + " 0 Tf";
field.setDefaultAppearance(fontInstruction + ' ' + colorOperator);
-
+
if (ctrl.box.getElement().hasAttribute("required")) {
field.setRequired(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("readonly")) {
field.setReadOnly(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("title")) {
field.setAlternateFieldName(ctrl.box.getElement().getAttribute("title"));
}
-
+
if (ctrl.box.getElement().getNodeName().equals("openhtmltopdf-combo")) {
field.setEdit(true);
field.setCombo(true);
}
-
+
PDAnnotationWidget widget = field.getWidgets().get(0);
- Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform, root, od);
- PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(), (float) rect2D.getWidth(), (float) rect2D.getHeight());
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform,
+ ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
widget.setRectangle(rect);
widget.setPage(ctrl.page);
widget.setPrinted(true);
-
+
ctrl.page.getAnnotations().add(widget);
}
-
- private void processHiddenControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root) throws IOException {
+
+ private void processHiddenControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root)
+ throws IOException {
PDTextField field = new PDTextField(acro);
setPartialNameToField(ctrl, field);
-
+
String value = ctrl.box.getElement().getAttribute("value");
-
+
field.setDefaultValue(value);
field.setValue(value);
-
+
// Even hidden fields need an associated widget to work.
PDAnnotationWidget widgy = field.getWidgets().get(0);
widgy.setPage(ctrl.page);
@@ -415,59 +425,99 @@ private void processHiddenControl(ControlFontPair pair, Control ctrl, PDAcroForm
widgy.setRectangle(new PDRectangle(0, 0, 1, 1));
ctrl.page.getAnnotations().add(widgy);
}
-
- private void processTextControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root) throws IOException {
+
+ private void processTextControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root)
+ throws IOException {
PDTextField field = new PDTextField(acro);
setPartialNameToField(ctrl, field);
-
+
FSColor color = ctrl.box.getStyle().getColor();
String colorOperator = getColorOperator(color);
String fontInstruction = "/" + pair.fontName + " 0 Tf";
field.setDefaultAppearance(fontInstruction + ' ' + colorOperator);
-
+
String value = ctrl.box.getElement().getNodeName().equals("textarea") ?
getTextareaText(ctrl.box.getElement()) :
ctrl.box.getElement().getAttribute("value");
-
+
field.setDefaultValue(value); // The reset value.
field.setValue(value); // The original value.
-
+
if (OpenUtil.parseIntegerOrNull(ctrl.box.getElement().getAttribute("max-length")) != null) {
field.setMaxLen(OpenUtil.parseIntegerOrNull(ctrl.box.getElement().getAttribute("max-length")));
}
-
+
if (ctrl.box.getElement().hasAttribute("required")) {
field.setRequired(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("readonly")) {
field.setReadOnly(true);
}
-
+
if (ctrl.box.getElement().getNodeName().equals("textarea")) {
field.setMultiline(true);
} else if (ctrl.box.getElement().getAttribute("type").equals("password")) {
field.setPassword(true);
} else if (ctrl.box.getElement().getAttribute("type").equals("file")) {
- XRLog.log(Level.WARNING, LogMessageId.LogMessageId0Param.GENERAL_PDF_ACROBAT_READER_DOES_NOT_SUPPORT_FORMS_WITH_FILE_INPUT);
+ XRLog.log(Level.WARNING,
+ LogMessageId.LogMessageId0Param.GENERAL_PDF_ACROBAT_READER_DOES_NOT_SUPPORT_FORMS_WITH_FILE_INPUT);
field.setFileSelect(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("title")) {
field.setAlternateFieldName(ctrl.box.getElement().getAttribute("title"));
}
-
+
PDAnnotationWidget widget = field.getWidgets().get(0);
- Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform, root, od);
- PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(), (float) rect2D.getWidth(), (float) rect2D.getHeight());
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform,
+ ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
widget.setRectangle(rect);
widget.setPage(ctrl.page);
widget.setPrinted(true);
-
+
+ ctrl.page.getAnnotations().add(widget);
+ }
+
+ private void processSignatureControl(ControlFontPair pair, Control ctrl, PDAcroForm acro, int i, Box root)
+ throws IOException {
+ PDSignatureField field = new PDSignatureField(acro);
+
+ setPartialNameToField(ctrl, field);
+
+ if (ctrl.box.getElement().hasAttribute("required")) {
+ field.setRequired(true);
+ }
+
+ if (ctrl.box.getElement().hasAttribute("readonly")) {
+ field.setReadOnly(true);
+ }
+
+ if (ctrl.box.getElement().hasAttribute("title")) {
+ field.setAlternateFieldName(ctrl.box.getElement().getAttribute("title"));
+ }
+ if (ctrl.box.getElement().hasAttribute("alt")) {
+ field.setPartialName(ctrl.box.getElement().getAttribute("alt"));
+ }
+
+ PDAnnotationWidget widget = field.getWidgets().get(0);
+
+ RenderingContext renderingContext = ctrl.c;
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(renderingContext, ctrl.box, ctrl.pageHeight, ctrl.transform,
+ ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
+
+ widget.setRectangle(rect);
+ widget.setPage(ctrl.page);
+ widget.setPrinted(true);
+
ctrl.page.getAnnotations().add(widget);
}
@@ -483,32 +533,32 @@ public enum CheckboxStyle {
STAR(72),
SQUARE(110);
-
+
private final int caption;
-
+
CheckboxStyle(int caption) {
this.caption = caption;
}
-
+
public static CheckboxStyle fromIdent(IdentValue id) {
if (id == IdentValue.CHECK)
return CHECK;
-
+
if (id == IdentValue.CROSS)
return CROSS;
-
+
if (id == IdentValue.SQUARE)
return SQUARE;
-
+
if (id == IdentValue.CIRCLE)
return CIRCLE;
-
+
if (id == IdentValue.DIAMOND)
return DIAMOND;
-
+
if (id == IdentValue.STAR)
return STAR;
-
+
return CHECK;
}
}
@@ -516,23 +566,18 @@ public static CheckboxStyle fromIdent(IdentValue id) {
/**
* Creates a checkbox appearance stream. Uses an ordinal of the zapf dingbats font for the check mark.
*/
- public static PDAppearanceStream createCheckboxAppearance(CheckboxStyle style, PDDocument doc, PDResources resources) {
- String appear =
- "q\n" +
- "BT\n" +
- "1 0 0 1 15 20 Tm\n" +
- "/OpenHTMLZap 100 Tf\n" +
- "(" + (char) style.caption + ") Tj\n" +
- "ET\n" +
- "Q\n";
-
+ public static PDAppearanceStream createCheckboxAppearance(CheckboxStyle style, PDDocument doc,
+ PDResources resources) {
+ String appear = "q\n" + "BT\n" + "1 0 0 1 15 20 Tm\n" + "/OpenHTMLZap 100 Tf\n" + "(" + (char) style.caption
+ + ") Tj\n" + "ET\n" + "Q\n";
+
return createCheckboxAppearance(appear, doc, resources);
}
-
+
public static PDAppearanceStream createCheckboxAppearance(String appear, PDDocument doc, PDResources resources) {
PDAppearanceStream s = new PDAppearanceStream(doc);
s.setBBox(new PDRectangle(100f, 100f));
- try (OutputStream os = s.getContentStream().createOutputStream()){
+ try (OutputStream os = s.getContentStream().createOutputStream()) {
os.write(appear.getBytes(StandardCharsets.US_ASCII));
} catch (IOException e) {
throw new PdfContentStreamAdapter.PdfException("createCheckboxAppearance", e);
@@ -547,12 +592,9 @@ private COSString getCOSStringUTF16Encoded(String value) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + 2);
out.write(0xFE); // BOM
out.write(0xFF); // BOM
- try
- {
+ try {
out.write(data);
- }
- catch (IOException e)
- {
+ } catch (IOException e) {
// should never happen
throw new RuntimeException(e);
}
@@ -560,8 +602,9 @@ private COSString getCOSStringUTF16Encoded(String value) {
COSString valueEncoded = new COSString(bytes);
return valueEncoded;
}
-
- private void processCheckboxControl(ControlFontPair pair, PDAcroForm acro, int i, Control ctrl, Box root) throws IOException {
+
+ private void processCheckboxControl(ControlFontPair pair, PDAcroForm acro, int i, Control ctrl, Box root)
+ throws IOException {
PDCheckBox field = new PDCheckBox(acro);
setPartialNameToField(ctrl, field);
@@ -569,46 +612,49 @@ private void processCheckboxControl(ControlFontPair pair, PDAcroForm acro, int i
if (ctrl.box.getElement().hasAttribute("required")) {
field.setRequired(true);
}
-
+
if (ctrl.box.getElement().hasAttribute("readonly")) {
field.setReadOnly(true);
}
-
+
/*
- * The only way I could get Acrobat Reader to display the checkbox checked properly was to
+ * The only way I could get Acrobat Reader to display the checkbox checked properly was to
* use an explicitly encoded unicode string for the OPT entry of the dictionary.
*/
COSArray arr = new COSArray();
arr.add(getCOSStringUTF16Encoded(ctrl.box.getElement().getAttribute("value")));
field.getCOSObject().setItem(COSName.OPT, arr);
-
+
if (ctrl.box.getElement().hasAttribute("title")) {
field.setAlternateFieldName(ctrl.box.getElement().getAttribute("title"));
}
-
+
COSName zero = COSName.getPDFName("0");
-
+
if (ctrl.box.getElement().hasAttribute("checked")) {
field.getCOSObject().setItem(COSName.AS, zero);
field.getCOSObject().setItem(COSName.V, zero);
field.getCOSObject().setItem(COSName.DV, zero);
} else {
- field.getCOSObject().setItem(COSName.AS, COSName.Off);
- field.getCOSObject().setItem(COSName.V, COSName.Off);
- field.getCOSObject().setItem(COSName.DV, COSName.Off);
+ field.getCOSObject().setItem(COSName.AS, COSName.Off);
+ field.getCOSObject().setItem(COSName.V, COSName.Off);
+ field.getCOSObject().setItem(COSName.DV, COSName.Off);
}
-
- Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform, root, od);
- PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(), (float) rect2D.getWidth(), (float) rect2D.getHeight());
+
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform,
+ ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
PDAnnotationWidget widget = field.getWidgets().get(0);
widget.setRectangle(rect);
widget.setPage(ctrl.page);
widget.setPrinted(true);
-
+
CheckboxStyle style = CheckboxStyle.fromIdent(ctrl.box.getStyle().getIdent(CSSName.FS_CHECKBOX_STYLE));
-
- PDAppearanceCharacteristicsDictionary appearanceCharacteristics = new PDAppearanceCharacteristicsDictionary(new COSDictionary());
+
+ PDAppearanceCharacteristicsDictionary appearanceCharacteristics = new PDAppearanceCharacteristicsDictionary(
+ new COSDictionary());
appearanceCharacteristics.setNormalCaption(String.valueOf((char) style.caption));
widget.setAppearanceCharacteristics(appearanceCharacteristics);
@@ -618,10 +664,10 @@ private void processCheckboxControl(ControlFontPair pair, PDAcroForm acro, int i
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
appearanceDict.getCOSObject().setItem(COSName.N, dict);
widget.setAppearance(appearanceDict);
-
+
ctrl.page.getAnnotations().add(widget);
}
-
+
private void processRadioButtonGroup(List group, PDAcroForm acro, int i, Box root) throws IOException {
String groupName = group.get(0).box.getElement().getAttribute("name");
PDRadioButton field = new PDRadioButton(acro);
@@ -629,33 +675,31 @@ private void processRadioButtonGroup(List group, PDAcroForm acro, int i
Field fObj = allFieldMap.get(groupName);
setPartialNameToField(group.get(0).box.getElement(), fObj, field);
- List values =
- group.stream()
- .map(ctrl -> ctrl.box.getElement().getAttribute("value"))
- .collect(Collectors.toList());
+ List values = group.stream().map(ctrl -> ctrl.box.getElement().getAttribute("value"))
+ .collect(Collectors.toList());
field.setExportValues(values);
// We can not make individual members of the group readonly so only make
// all radio buttons in group readonly if they are all marked readonly.
- boolean readonly =
- group.stream()
- .allMatch(ctrl -> ctrl.box.getElement().hasAttribute("readonly"));
+ boolean readonly = group.stream().allMatch(ctrl -> ctrl.box.getElement().hasAttribute("readonly"));
field.setReadOnly(readonly);
List widgets = new ArrayList<>(group.size());
-
+
int radioCnt = 0;
-
+
for (Control ctrl : group) {
- Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform, root, od);
- PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(), (float) rect2D.getWidth(), (float) rect2D.getHeight());
-
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight,
+ ctrl.transform, ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
+
PDAnnotationWidget widget = new PDAnnotationWidget();
-
+
widget.setRectangle(rect);
widget.setPage(ctrl.page);
widget.setPrinted(true);
-
+
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.getPDFName("" + radioCnt), docFormsStateContainer.getRadioOnStream());
dict.setItem(COSName.Off, docFormsStateContainer.getRadioOffStream());
@@ -669,10 +713,10 @@ private void processRadioButtonGroup(List group, PDAcroForm acro, int i
}
widget.setAppearance(appearanceDict);
-
+
widgets.add(widget);
ctrl.page.getAnnotations().add(widget);
-
+
radioCnt++;
}
@@ -680,11 +724,11 @@ private void processRadioButtonGroup(List group, PDAcroForm acro, int i
for (Control ctrl : group) {
if (ctrl.box.getElement().hasAttribute("checked")) {
- field.setValue(ctrl.box.getElement().getAttribute("value"));
+ field.setValue(ctrl.box.getElement().getAttribute("value"));
}
}
}
-
+
private void processSubmitControl(PDAcroForm acro, int i, Control ctrl, Box root) throws IOException {
final int FLAG_USE_GET = 1 << 3;
final int FLAG_USE_HTML_SUBMIT = 1 << 2;
@@ -694,17 +738,17 @@ private void processSubmitControl(PDAcroForm acro, int i, Control ctrl, Box root
if (ctrl.box.getElement().hasAttribute("name")) {
// Buttons can't have a value so we create a hidden text field instead.
PDTextField field = new PDTextField(acro);
-
+
Field fObj = allFieldMap.get(ctrl.box.getElement().getAttribute("name"));
fObj.field = field;
-
+
field.setPartialName(fObj.partialName);
-
+
String value = ctrl.box.getElement().getAttribute("value");
-
+
field.setDefaultValue(value);
field.setValue(value);
-
+
// Even hidden fields need an associated widget to work.
PDAnnotationWidget widgy = field.getWidgets().get(0);
widgy.setPage(ctrl.page);
@@ -712,14 +756,16 @@ private void processSubmitControl(PDAcroForm acro, int i, Control ctrl, Box root
widgy.setRectangle(new PDRectangle(0, 0, 1, 1));
ctrl.page.getAnnotations().add(widgy);
}
-
+
// We use an internal name so as not to conflict with a hidden text that we just created.
btn.setPartialName("OpenHTMLCtrl" + i);
-
+
PDAnnotationWidget widget = btn.getWidgets().get(0);
-
- Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform, root, od);
- PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(), (float) rect2D.getWidth(), (float) rect2D.getHeight());
+
+ Rectangle2D rect2D = PdfBoxFastLinkManager.createTargetArea(ctrl.c, ctrl.box, ctrl.pageHeight, ctrl.transform,
+ ctrl.pageBox, od);
+ PDRectangle rect = new PDRectangle((float) rect2D.getMinX(), (float) rect2D.getMinY(),
+ (float) rect2D.getWidth(), (float) rect2D.getHeight());
widget.setRectangle(rect);
widget.setPage(ctrl.page);
@@ -730,15 +776,16 @@ private void processSubmitControl(PDAcroForm acro, int i, Control ctrl, Box root
fieldsToInclude.add(f.qualifiedName);
}
}
-
+
if (ctrl.box.getElement().getAttribute("type").equals("reset")) {
PDActionResetForm reset = new PDActionResetForm();
reset.setFields(fieldsToInclude.getCOSArray());
- widget.setAction(reset);;
+ widget.setAction(reset);
+ ;
} else {
PDFileSpecification fs = PDFileSpecification.createFS(new COSString(element.getAttribute("action")));
PDActionSubmitForm submit = new PDActionSubmitForm();
-
+
submit.setFields(fieldsToInclude.getCOSArray());
submit.setFile(fs);
@@ -778,82 +825,85 @@ private static void setPartialNameToField(Element element, Field fObj, PDField f
XRLog.log(Level.WARNING, LogMessageId.LogMessageId2Param.GENERAL_PDF_FOUND_ELEMENT_WITHOUT_ATTRIBUTE_NAME, element.getTagName(), sb.toString());
}
}
-
+
public int process(PDAcroForm acro, int startId, Box root) throws IOException {
processControlNames();
-
- int i = startId;
+
+ int i = startId;
for (ControlFontPair pair : controls) {
i++;
-
+
Control ctrl = pair.control;
Element e = ctrl.box.getElement();
-
- if ((e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("text")) ||
- (e.getNodeName().equals("textarea")) ||
- (e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("password")) ||
- (e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("file"))) {
+
+
+ if ((e.getNodeName().equals("input") && e.getAttribute("type").equals("text")
+ && e.getAttribute("class").contains("signature"))) {
+
+ processSignatureControl(pair, ctrl, acro, i, root);
+
+ } else if ((e.getNodeName().equals("input") && e.getAttribute("type").equals("text"))
+ || (e.getNodeName().equals("textarea"))
+ || (e.getNodeName().equals("input") && e.getAttribute("type").equals("password"))
+ || (e.getNodeName().equals("input") && e.getAttribute("type").equals("file"))) {
// Start with the text controls (text, password, file and textarea).
processTextControl(pair, ctrl, acro, i, root);
} else if ((e.getNodeName().equals("select") &&
- !e.hasAttribute("multiple")) ||
- (e.getNodeName().equals("openhtmltopdf-combo"))) {
-
+ !e.hasAttribute("multiple")) ||
+ (e.getNodeName().equals("openhtmltopdf-combo"))) {
+
processSelectControl(pair, ctrl, acro, i, root);
} else if (e.getNodeName().equals("select") &&
- e.hasAttribute("multiple")) {
-
+ e.hasAttribute("multiple")) {
+
processMultiSelectControl(pair, ctrl, acro, i, root);
} else if (e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("checkbox")) {
-
+ e.getAttribute("type").equals("checkbox")) {
+
processCheckboxControl(pair, acro, i, ctrl, root);
} else if (e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("hidden")) {
-
+ e.getAttribute("type").equals("hidden")) {
+
processHiddenControl(pair, ctrl, acro, i, root);
} else if (e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("radio")) {
+ e.getAttribute("type").equals("radio")) {
// We have to do radio button groups in one hit so add them to a map of list keyed on name.
List radioGroup = radioGroups.get(e.getAttribute("name"));
-
+
if (radioGroup == null) {
radioGroup = new ArrayList<>();
radioGroups.put(e.getAttribute("name"), radioGroup);
}
radioGroup.add(ctrl);
- } else if ((e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("submit")) ||
- (e.getNodeName().equals("button") &&
- !e.getAttribute("type").equals("button")) ||
- (e.getNodeName().equals("input") &&
- e.getAttribute("type").equals("reset"))) {
+ } else if ((e.getNodeName().equals("input") &&
+ e.getAttribute("type").equals("submit")) ||
+ (e.getNodeName().equals("button") &&
+ !e.getAttribute("type").equals("button")) ||
+ (e.getNodeName().equals("input") &&
+ e.getAttribute("type").equals("reset"))) {
// We've got a submit or reset control for this form.
submits.add(ctrl);
}
}
-
+
// Now process each group of radio buttons.
for (List group : radioGroups.values()) {
i++;
processRadioButtonGroup(group, acro, i, root);
}
-
+
// We do submit controls last as we need all the fields in this form.
for (Control ctrl : submits) {
i++;
processSubmitControl(acro, i, ctrl, root);
}
-
+
createNonTerminalFields(acro);
-
+
return i;
}
}
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxPerDocumentFormState.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxPerDocumentFormState.java
index cea67bded..8262bb85a 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxPerDocumentFormState.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxPerDocumentFormState.java
@@ -95,8 +95,8 @@ public void addControlIfRequired(Box box, PDPage page, AffineTransform transform
}
}
- private String getControlFont(SharedContext sharedContext, PdfBoxForm.Control ctrl) {
- PDFont fnt = ((PdfBoxFSFont) sharedContext.getFont(ctrl.box.getStyle().getFontSpecification())).getFontDescription().get(0).getFont();
+ private String getControlFont(SharedContext sharedContext, PdfBoxForm.Control ctrl, RenderingContext renderingContext) {
+ PDFont fnt = ((PdfBoxFSFont) sharedContext.getFont(ctrl.box.getStyle().getFont(renderingContext))).getFontDescription().get(0).getFont();
String fontName;
if (!controlFonts.containsKey(fnt)) {
@@ -139,14 +139,14 @@ private void createCheckboxFontResource() {
}
}
- public void processControls(SharedContext sharedContext, PDDocument writer, Box root) {
+ public void processControls(SharedContext sharedContext, PDDocument writer, Box root, RenderingContext renderingContext) {
for (PdfBoxForm.Control ctrl : controls) {
PdfBoxForm frm = findEnclosingForm(ctrl.box.getElement());
String fontName = null;
if (!ArrayUtil.isOneOf(ctrl.box.getElement().getAttribute("type"), "checkbox", "radio", "hidden")) {
// Need to embed a font for every control other than checkbox, radio and hidden.
- fontName = getControlFont(sharedContext, ctrl);
+ fontName = getControlFont(sharedContext, ctrl, renderingContext);
} else if (ctrl.box.getElement().getAttribute("type").equals("checkbox")) {
createCheckboxFontResource();
createCheckboxAppearanceStreams(writer, ctrl);
diff --git a/openhtmltopdf-rtl-support/pom.xml b/openhtmltopdf-rtl-support/pom.xml
index a6a00c933..6b311cc3b 100644
--- a/openhtmltopdf-rtl-support/pom.xml
+++ b/openhtmltopdf-rtl-support/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-rtl-support
diff --git a/openhtmltopdf-slf4j/pom.xml b/openhtmltopdf-slf4j/pom.xml
index b6e9ebb4c..90e07db07 100644
--- a/openhtmltopdf-slf4j/pom.xml
+++ b/openhtmltopdf-slf4j/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-slf4j
diff --git a/openhtmltopdf-svg-support/pom.xml b/openhtmltopdf-svg-support/pom.xml
index 2bb39b195..62e4a8073 100644
--- a/openhtmltopdf-svg-support/pom.xml
+++ b/openhtmltopdf-svg-support/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-svg-support
diff --git a/openhtmltopdf-templates/pom.xml b/openhtmltopdf-templates/pom.xml
index 69a95b7b4..0f4bb5ec4 100644
--- a/openhtmltopdf-templates/pom.xml
+++ b/openhtmltopdf-templates/pom.xml
@@ -6,7 +6,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
openhtmltopdf-templates
diff --git a/pom.xml b/pom.xml
index df5eb1e7c..77691825a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.openhtmltopdf
openhtmltopdf-parent
- 1.0.11-SNAPSHOT
+ 1.0.11.20240615
pom