mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Fix form-fill hang when flattening with empty values (#6143)
This commit is contained in:
@@ -629,11 +629,12 @@ public class FormUtils {
|
|||||||
}
|
}
|
||||||
log.debug("Skipping form fill because document has no AcroForm");
|
log.debug("Skipping form fill because document has no AcroForm");
|
||||||
if (flatten) {
|
if (flatten) {
|
||||||
flattenEntireDocument(document, null);
|
flattenEntireDocument(document, null, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean valuesApplied = false;
|
||||||
if (values != null && !values.isEmpty()) {
|
if (values != null && !values.isEmpty()) {
|
||||||
acroForm.setCacheFields(true);
|
acroForm.setCacheFields(true);
|
||||||
|
|
||||||
@@ -667,18 +668,26 @@ public class FormUtils {
|
|||||||
Object rawValue = entry.getValue();
|
Object rawValue = entry.getValue();
|
||||||
String value = rawValue == null ? null : Objects.toString(rawValue, null);
|
String value = rawValue == null ? null : Objects.toString(rawValue, null);
|
||||||
applyValueToField(field, value, strict);
|
applyValueToField(field, value, strict);
|
||||||
|
valuesApplied = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureAppearances(acroForm);
|
if (valuesApplied) {
|
||||||
|
ensureAppearances(acroForm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repairWidgetGeometry(document, acroForm);
|
repairWidgetGeometry(document, acroForm);
|
||||||
|
|
||||||
if (flatten) {
|
if (flatten) {
|
||||||
flattenEntireDocument(document, acroForm);
|
flattenEntireDocument(document, acroForm, valuesApplied);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cap the fallback rendering DPI. This path only runs when acroForm.flatten()
|
||||||
|
// throws, and the goal is a readable flattened document — not print quality —
|
||||||
|
// so clamping avoids runaway memory/CPU on pathological inputs.
|
||||||
|
private static final int FLATTEN_FALLBACK_MAX_DPI = 200;
|
||||||
|
|
||||||
private void flattenViaRendering(PDDocument document, PDAcroForm acroForm) throws IOException {
|
private void flattenViaRendering(PDDocument document, PDAcroForm acroForm) throws IOException {
|
||||||
if (document == null) {
|
if (document == null) {
|
||||||
return;
|
return;
|
||||||
@@ -704,28 +713,34 @@ public class FormUtils {
|
|||||||
properties != null && properties.getSystem() != null
|
properties != null && properties.getSystem() != null
|
||||||
? properties.getSystem().getMaxDPI()
|
? properties.getSystem().getMaxDPI()
|
||||||
: 300;
|
: 300;
|
||||||
|
int effectiveDpi = Math.min(requestedDpi, FLATTEN_FALLBACK_MAX_DPI);
|
||||||
|
|
||||||
rebuildDocumentFromImages(document, renderer, requestedDpi);
|
rebuildDocumentFromImages(document, renderer, effectiveDpi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: this implementation suffers from:
|
// Use PDFBox's built-in field flattening which bakes form field values
|
||||||
// https://issues.apache.org/jira/browse/PDFBOX-5962
|
// into the page content stream as static text/graphics, removing the
|
||||||
private void flattenEntireDocument(PDDocument document, PDAcroForm acroForm)
|
// interactive form structure but preserving all other document content
|
||||||
throws IOException {
|
// (images, text, annotations, etc.) at full quality.
|
||||||
if (document == null) {
|
//
|
||||||
|
// Forcing appearance regeneration via setNeedAppearances(true) drives
|
||||||
|
// PDFBox into refreshAppearances inside flatten(), where it can hang on
|
||||||
|
// certain documents (PDFBOX-5962). We therefore only regenerate when we
|
||||||
|
// actually wrote new values, or when some widgets are missing appearance
|
||||||
|
// streams and would otherwise flatten blank.
|
||||||
|
private void flattenEntireDocument(
|
||||||
|
PDDocument document, PDAcroForm acroForm, boolean valuesWritten) throws IOException {
|
||||||
|
if (document == null || acroForm == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (acroForm == null) {
|
if (valuesWritten || hasWidgetWithoutAppearance(acroForm)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use PDFBox's built-in field flattening which bakes form field values
|
|
||||||
// into the page content stream as static text/graphics, removing the
|
|
||||||
// interactive form structure but preserving all other document content
|
|
||||||
// (images, text, annotations, etc.) at full quality.
|
|
||||||
try {
|
|
||||||
ensureAppearances(acroForm);
|
ensureAppearances(acroForm);
|
||||||
|
} else {
|
||||||
|
acroForm.setNeedAppearances(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
acroForm.flatten();
|
acroForm.flatten();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(
|
log.warn(
|
||||||
@@ -736,6 +751,28 @@ public class FormUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasWidgetWithoutAppearance(PDAcroForm acroForm) {
|
||||||
|
for (PDField field : acroForm.getFieldTree()) {
|
||||||
|
if (!(field instanceof PDTerminalField terminalField)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<PDAnnotationWidget> widgets = terminalField.getWidgets();
|
||||||
|
if (widgets == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (PDAnnotationWidget widget : widgets) {
|
||||||
|
if (widget == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PDAppearanceDictionary appearance = widget.getAppearance();
|
||||||
|
if (appearance == null || appearance.getNormalAppearance() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void rebuildDocumentFromImages(PDDocument document, PDFRenderer renderer, int dpi)
|
private void rebuildDocumentFromImages(PDDocument document, PDFRenderer renderer, int dpi)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
int pageCount = document.getNumberOfPages();
|
int pageCount = document.getNumberOfPages();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stirling.software.common.util;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -249,6 +250,60 @@ class FormUtilsAdditionalTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression: PDFBOX-5962. Flattening with an empty values map used to force
|
||||||
|
// setNeedAppearances(true), triggering PDFBox's refreshAppearances loop which
|
||||||
|
// could hang indefinitely. The call must complete quickly and clear form fields.
|
||||||
|
@Test
|
||||||
|
void testApplyFieldValues_emptyValuesWithFlatten_completesAndFlattens() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
SetupDocument setup = createBasicDocument(doc);
|
||||||
|
PDTextField textField = new PDTextField(setup.acroForm);
|
||||||
|
textField.setPartialName("company");
|
||||||
|
attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20));
|
||||||
|
|
||||||
|
assertTrue(setup.acroForm.getNeedAppearances());
|
||||||
|
|
||||||
|
assertTimeoutPreemptively(
|
||||||
|
Duration.ofSeconds(10),
|
||||||
|
() -> FormUtils.applyFieldValues(doc, Map.of(), true, false));
|
||||||
|
|
||||||
|
PDAcroForm after = doc.getDocumentCatalog().getAcroForm();
|
||||||
|
assertTrue(after == null || after.getFields().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testApplyFieldValues_nullValuesWithFlatten_completesAndFlattens() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
SetupDocument setup = createBasicDocument(doc);
|
||||||
|
PDTextField textField = new PDTextField(setup.acroForm);
|
||||||
|
textField.setPartialName("company");
|
||||||
|
attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20));
|
||||||
|
|
||||||
|
assertTimeoutPreemptively(
|
||||||
|
Duration.ofSeconds(10),
|
||||||
|
() -> FormUtils.applyFieldValues(doc, null, true, false));
|
||||||
|
|
||||||
|
PDAcroForm after = doc.getDocumentCatalog().getAcroForm();
|
||||||
|
assertTrue(after == null || after.getFields().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testApplyFieldValues_valuesWithFlatten_appliesValueAndFlattens() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
SetupDocument setup = createBasicDocument(doc);
|
||||||
|
PDTextField textField = new PDTextField(setup.acroForm);
|
||||||
|
textField.setPartialName("company");
|
||||||
|
attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20));
|
||||||
|
|
||||||
|
FormUtils.applyFieldValues(doc, Map.of("company", "Stirling"), true, false);
|
||||||
|
|
||||||
|
PDAcroForm after = doc.getDocumentCatalog().getAcroForm();
|
||||||
|
assertTrue(after == null || after.getFields().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- filterSingleChoiceSelection ---
|
// --- filterSingleChoiceSelection ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user