From 115fa00e824ff1cec69542217512432b4172be04 Mon Sep 17 00:00:00 2001 From: Edi Weissmann Date: Wed, 20 Mar 2024 15:39:03 +0100 Subject: [PATCH] ref #431 Merge breaks form field rendering/formatting in Mac Preview Only removes key/value pairs from the widget dict when the acroFormPolicy is MERGE; no longer removes for MERGE_RENAMING and FLATTEN. --- .../sambox/component/AcroFormsMerger.java | 22 +++++++++------ .../impl/sambox/MergeSamboxTaskTest.java | 25 +++++++++++++++++- .../pdf/forms/form_field_centered_text.pdf | Bin 0 -> 2091 bytes 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 sejda-tests/src/main/resources/pdf/forms/form_field_centered_text.pdf diff --git a/sejda-sambox/src/main/java/org/sejda/impl/sambox/component/AcroFormsMerger.java b/sejda-sambox/src/main/java/org/sejda/impl/sambox/component/AcroFormsMerger.java index df434431b..554a326db 100644 --- a/sejda-sambox/src/main/java/org/sejda/impl/sambox/component/AcroFormsMerger.java +++ b/sejda-sambox/src/main/java/org/sejda/impl/sambox/component/AcroFormsMerger.java @@ -249,14 +249,14 @@ public void mergeForm(PDAcroForm originalForm, LookupTable annotat switch (policy) { case MERGE_RENAMING_EXISTING_FIELDS: updateForm(originalForm, annotationsLookup, createRenamingTerminalField, - createRenamingNonTerminalField); + createRenamingNonTerminalField, false); break; case MERGE: - updateForm(originalForm, annotationsLookup, createOrReuseTerminalField, createOrReuseNonTerminalField); + updateForm(originalForm, annotationsLookup, createOrReuseTerminalField, createOrReuseNonTerminalField, true); break; case FLATTEN: updateForm(originalForm, annotationsLookup, createRenamingTerminalField, - createRenamingNonTerminalField); + createRenamingNonTerminalField, false); flatten(); break; default: @@ -286,7 +286,7 @@ private void removeFieldKeysFromWidgets(Collection annotatio private void updateForm(PDAcroForm originalForm, LookupTable annotationsLookup, BiFunction, PDTerminalField> getTerminalField, - BiConsumer> createNonTerminalField) { + BiConsumer> createNonTerminalField, boolean shouldRemoveFieldKeysFromWidgets) { AcroFormUtils.mergeDefaults(originalForm, form); LookupTable fieldsLookup = new LookupTable<>(); Set allRelevantWidgets = annotationsLookup.keys().stream() @@ -297,7 +297,7 @@ private void updateForm(PDAcroForm originalForm, LookupTable annot // every widget we meet is removed from the allRelevantWidgets so we can identify widgets not referenced by the originalForm originalForm.getFieldTree().stream().forEach( f -> mergeField(f, annotationsLookup, getTerminalField, createNonTerminalField, fieldsLookup, - of(allRelevantWidgets::remove))); + of(allRelevantWidgets::remove), shouldRemoveFieldKeysFromWidgets)); // keep track of the root fields originalForm.getFields().stream().map(fieldsLookup::lookup).filter(Objects::nonNull).forEach(rootFields::add); @@ -316,7 +316,7 @@ private void updateForm(PDAcroForm originalForm, LookupTable annot }); dummy.getFieldTree().stream().forEach(f -> mergeField(f, annotationsLookup, getTerminalField, - createNonTerminalField, fieldsLookup, empty())); + createNonTerminalField, fieldsLookup, empty(), shouldRemoveFieldKeysFromWidgets)); // keep track of the root fields dummy.getFields().stream().map(fieldsLookup::lookup).filter(Objects::nonNull).forEach(rootFields::add); } @@ -329,7 +329,7 @@ private void updateForm(PDAcroForm originalForm, LookupTable annot private void mergeField(PDField field, LookupTable annotationsLookup, BiFunction, PDTerminalField> getTerminalField, BiConsumer> createNonTerminalField, LookupTable fieldsLookup, - Optional> onProcessedWidget) { + Optional> onProcessedWidget, boolean shouldRemoveFieldKeysFromWidgets) { if (!field.isTerminal()) { createNonTerminalField.accept(field, fieldsLookup); } else { @@ -337,7 +337,13 @@ private void mergeField(PDField field, LookupTable annotationsLook if (!relevantWidgets.isEmpty()) { PDTerminalField terminalField = getTerminalField.apply((PDTerminalField) field, fieldsLookup); if (nonNull(terminalField)) { - removeFieldKeysFromWidgets(relevantWidgets); + // removal is no longer performed for MERGE_RENAMING or FLATTEN policies + // we currently only perform this removal when policy is MERGE (backwards compatibility) + // TODO: review if this removal should be peformed for the MERGE policy + if(shouldRemoveFieldKeysFromWidgets) { + removeFieldKeysFromWidgets(relevantWidgets); + } + for (PDAnnotationWidget widget : relevantWidgets) { terminalField.addWidgetIfMissing(widget); onProcessedWidget.ifPresent(c -> field.getWidgets().forEach(c)); diff --git a/sejda-sambox/src/test/java/org/sejda/impl/sambox/MergeSamboxTaskTest.java b/sejda-sambox/src/test/java/org/sejda/impl/sambox/MergeSamboxTaskTest.java index 0586c6bad..438d08047 100644 --- a/sejda-sambox/src/test/java/org/sejda/impl/sambox/MergeSamboxTaskTest.java +++ b/sejda-sambox/src/test/java/org/sejda/impl/sambox/MergeSamboxTaskTest.java @@ -40,12 +40,15 @@ import org.sejda.model.scale.PageNormalizationPolicy; import org.sejda.model.task.Task; import org.sejda.model.toc.ToCPolicy; +import org.sejda.sambox.cos.COSName; import org.sejda.sambox.pdmodel.PDDocument; import org.sejda.sambox.pdmodel.PDPage; import org.sejda.sambox.pdmodel.PageNotFoundException; import org.sejda.sambox.pdmodel.common.PDPageLabelRange; import org.sejda.sambox.pdmodel.common.PDPageLabels; import org.sejda.sambox.pdmodel.common.PDRectangle; +import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotation; +import org.sejda.sambox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.sejda.sambox.pdmodel.interactive.form.PDField; import org.sejda.sambox.text.PDFTextStripperByArea; import org.sejda.tests.DocBuilder; @@ -1088,8 +1091,28 @@ public void withLargeTocItemsThatWrapAndGenerateMultipleTocPages() throws IOExce assertFooterHasText(d.getPage(27), "Attachment 26 - Sample file name - shorter 28"); }); } + + @Test + public void form_field_loses_formatting_in_Mac_Preview() throws IOException { + MergeParameters parameters = new MergeParameters(); + parameters.setExistingOutputPolicy(ExistingOutputPolicy.OVERWRITE); + parameters.addInput(new PdfMergeInput(customInput("pdf/forms/form_field_centered_text.pdf"))); + parameters.setAcroFormPolicy(AcroFormPolicy.MERGE_RENAMING_EXISTING_FIELDS); + + testContext.pdfOutputTo(parameters); + execute(parameters); + + testContext.assertTaskCompleted(); + testContext.assertPages(1).forEachPdfOutput(d -> { + PDPage page = d.getPage(0); + PDAnnotation annotation = page.getAnnotations().get(0); + PDAnnotationWidget widget = (PDAnnotationWidget) annotation; + assertEquals(1, widget.getCOSObject().getInt(COSName.Q)); + assertEquals("/Helv 8 Tf 0.718 0.11 0.11 rg", widget.getCOSObject().getString(COSName.DA)); + }); + } - private float widthOfCropBox(PDPage page) { + private float widthOfCropBox(PDPage page) { return page.getCropBox().rotate(page.getRotation()).getWidth(); } diff --git a/sejda-tests/src/main/resources/pdf/forms/form_field_centered_text.pdf b/sejda-tests/src/main/resources/pdf/forms/form_field_centered_text.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8f49ab3e4a8cbf2ff13cd9f484fd9b7abf7ef901 GIT binary patch literal 2091 zcmb7FdsIw$9PgphRZ}O6v14^Bksj{Motb84jX6DTQ=?MNBq`LGE~c8fHFKv@t1U_I zqE)F_v3emyv9?(3tPPcLj$(GD$V#2idM!QnPV6~4IvoG~&iTE*pV#+m>yPlAXiJ!Y zZT0h&?Hn0a1 zfLtXCjE^Z~Aai1$#{*D#g!UmC^@A7r(r6r2fDk`gjH3uD!6Hxq@hWGY{U5FMYp5Ibmjvvh#;xDbO9~(!87y&kp&ARg~8rPAV zjoxH4;ko52x$EwcWKcahw%wSS?&iU`pSX3@@L2MGbzu=RZh*zP(3QyQi3*-WUi5q4 z^}hAlt=4%^c5?kK$!lHU^gkQ^hXu$m@Ed9qjDT%1JEbwqfKT{5+1Ijqoc2*J5~K3tNd~O zS~USkh$=CaLP7`+zgOPP4NCw+wW`usbdbha`~{IXKqE?-V}2ze)v3h6q=Vn#ncUtL-kdtNva%Kt+DMVGvO;8JOXmts zn542L`-!+Hp@pj`C@pEe^g4w;D6x``>$I(bET-Z~tHtCre9Y6A=Mo%nAv~4lKsKzO8X_Jz`L?ot<54tTx^pD7QX8&0>Yh(8AZ3LtgA^ zM-55s2(PKGaBRO^8M4H@y}hfZzVtBV(Tm#+wK+2R4h#&VQeV5bOJ-dq$swvKUEbG&`trGNI&CJ|W=XUF01I03@iA=ecx%>q0 zR5)F8OSAB_(@vl9+j&k`9P8qbKX?%JlOdnd3kR^Nn|w|j*k5$AC@QP|Xv9#NY^ZAW zO#{x1*@DO4lMWxMdQ_-RGLq{?*GpzR;xs9{V@G?3dF3Mqd!Lyn4xaprJM*rAfg!Ed zYmekLBg$IMn_U{F-#tFi^YX3nAmzED45NgmV82$&gh^Eoc22Wk?F%*XcK*h1x>==O zQ=+x-#wh2`zEuu!^~w_E-zs&?vA2-xtdM52hB(&CLucC0I^;B}u!HK#3HHrG~K(92T=J%Oeoc>yy_zjdj#J-91i3-ch9k?CuG?$ixD$Zo)zwUH; zQ=R;D+{z|}%XpKdwj8r(?wBg6TFYva>P)no_mccbXB8MqBYua8CE^3pn4d`^n>{)Dwryu-rBKb?UF%FAo-6R1g> zHxdg2L4FSXTiU`3r90N-TIw6khg+<6=LasX*ak#ysz@g#lEWFA`({bj$=>=iTaTZQ j?8)tn@wRl)GxDB-PbDr^;0X!>0F6a3YHRD^#|Qoc0{R=^ literal 0 HcmV?d00001