diff --git a/.gitignore b/.gitignore index 71bf26eee..603ce6260 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ Maven_Ant_Builder.launch /openhtmltopdf-core/nbproject/ /.vscode/ /.metadata/ +openhtmltopdf-examples/.factorypath diff --git a/openhtmltopdf-core/pom.xml b/openhtmltopdf-core/pom.xml index 25fd7a223..0db0877f4 100644 --- a/openhtmltopdf-core/pom.xml +++ b/openhtmltopdf-core/pom.xml @@ -6,7 +6,7 @@ com.openhtmltopdf openhtmltopdf-parent - 1.0.11-SNAPSHOT + 1.0.11.20240615 openhtmltopdf-core diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java index 0955d36d2..da36db41a 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/newtable/TableBox.java @@ -650,6 +650,9 @@ protected int getCSSWidth(CssContext c) { RectPropertySet padding = getPadding(c); result -= (int)padding.left() + (int)padding.right(); } + + RectPropertySet margin = getMargin(c); + result -= (int)margin.left() + (int)margin.right(); return result >= 0 ? result : -1; } diff --git a/openhtmltopdf-examples/pom.xml b/openhtmltopdf-examples/pom.xml index f82f46d66..02ba40482 100644 --- a/openhtmltopdf-examples/pom.xml +++ b/openhtmltopdf-examples/pom.xml @@ -6,7 +6,7 @@ com.openhtmltopdf openhtmltopdf-parent - 1.0.11-SNAPSHOT + 1.0.11.20240615 openhtmltopdf-examples diff --git a/openhtmltopdf-examples/src/main/resources/testcases/form-controls.html b/openhtmltopdf-examples/src/main/resources/testcases/form-controls.html index 6035daaf5..2e6bb3bf0 100644 --- a/openhtmltopdf-examples/src/main/resources/testcases/form-controls.html +++ b/openhtmltopdf-examples/src/main/resources/testcases/form-controls.html @@ -4,6 +4,7 @@ input { color: orange; font-family: monospace; } textarea { color: red; font-family: monospace; } button { color: blue; font-family: monospace; border-radius: 8px; background-color: yellow; } +input[name="signature"] { color: green; } @@ -20,6 +21,7 @@ +
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 @@ + + + + + +
+
Text
+ +
Text
+ +
+ + 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 @@ + + + + + + +
+
+
+ Required signature please: + +
+
+ + +
Another one
+
+ +
+
+ + 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 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