mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +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");
|
||||
if (flatten) {
|
||||
flattenEntireDocument(document, null);
|
||||
flattenEntireDocument(document, null, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
boolean valuesApplied = false;
|
||||
if (values != null && !values.isEmpty()) {
|
||||
acroForm.setCacheFields(true);
|
||||
|
||||
@@ -667,18 +668,26 @@ public class FormUtils {
|
||||
Object rawValue = entry.getValue();
|
||||
String value = rawValue == null ? null : Objects.toString(rawValue, null);
|
||||
applyValueToField(field, value, strict);
|
||||
valuesApplied = true;
|
||||
}
|
||||
|
||||
ensureAppearances(acroForm);
|
||||
if (valuesApplied) {
|
||||
ensureAppearances(acroForm);
|
||||
}
|
||||
}
|
||||
|
||||
repairWidgetGeometry(document, acroForm);
|
||||
|
||||
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 {
|
||||
if (document == null) {
|
||||
return;
|
||||
@@ -704,28 +713,34 @@ public class FormUtils {
|
||||
properties != null && properties.getSystem() != null
|
||||
? properties.getSystem().getMaxDPI()
|
||||
: 300;
|
||||
int effectiveDpi = Math.min(requestedDpi, FLATTEN_FALLBACK_MAX_DPI);
|
||||
|
||||
rebuildDocumentFromImages(document, renderer, requestedDpi);
|
||||
rebuildDocumentFromImages(document, renderer, effectiveDpi);
|
||||
}
|
||||
|
||||
// note: this implementation suffers from:
|
||||
// https://issues.apache.org/jira/browse/PDFBOX-5962
|
||||
private void flattenEntireDocument(PDDocument document, PDAcroForm acroForm)
|
||||
throws IOException {
|
||||
if (document == null) {
|
||||
// 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.
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (acroForm == null) {
|
||||
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 {
|
||||
if (valuesWritten || hasWidgetWithoutAppearance(acroForm)) {
|
||||
ensureAppearances(acroForm);
|
||||
} else {
|
||||
acroForm.setNeedAppearances(false);
|
||||
}
|
||||
|
||||
try {
|
||||
acroForm.flatten();
|
||||
} catch (Exception e) {
|
||||
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)
|
||||
throws IOException {
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
@@ -3,6 +3,7 @@ package stirling.software.common.util;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
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 ---
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user