fix: derive choice field label from field name, not first option
This commit is contained in:
@@ -1464,16 +1464,29 @@ public class FormUtils {
|
||||
return tooltipLabel;
|
||||
}
|
||||
|
||||
// Only check options for choice-type fields (combobox, listbox, radio)
|
||||
if (CHOICE_FIELD_TYPES.contains(type) && options != null && !options.isEmpty()) {
|
||||
// A clearly meaningful field name describes the whole field and matches
|
||||
// the name shown in the editor, so it is the best label.
|
||||
String humanized = cleanLabel(humanizeName(name));
|
||||
if (humanized != null && !looksGeneric(humanized)) {
|
||||
return humanized;
|
||||
}
|
||||
|
||||
// Only when the name is an auto-generated identifier ("Field_5", "t12", a
|
||||
// UUID) is an option more descriptive than the name. A human-typed name -
|
||||
// even a generic-sounding word like "Choice" or "Name" - is preferred just
|
||||
// below so the viewer label stays in sync with the editor's field name. This
|
||||
// stops a radio group named "Choice" with options Yes/No reading as "Yes".
|
||||
if (CHOICE_FIELD_TYPES.contains(type)
|
||||
&& options != null
|
||||
&& !options.isEmpty()
|
||||
&& looksAutoGenerated(name)) {
|
||||
String optionCandidate = cleanLabel(options.get(0));
|
||||
if (optionCandidate != null && !looksGeneric(optionCandidate)) {
|
||||
return optionCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
String humanized = cleanLabel(humanizeName(name));
|
||||
if (humanized != null && !looksGeneric(humanized)) {
|
||||
if (humanized != null && !looksAutoGenerated(name)) {
|
||||
return humanized;
|
||||
}
|
||||
|
||||
@@ -1510,6 +1523,29 @@ public class FormUtils {
|
||||
|| patterns.getOptionalTNumericPattern().matcher(simplified).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* True only for structurally auto-generated identifiers - "Field_5", "F12", "t3", bare numbers,
|
||||
* UUIDs. Unlike {@link #looksGeneric}, this does NOT reject human-typed placeholder words such
|
||||
* as "Choice", "Name" or "Value"; those are still usable labels and must not be discarded in
|
||||
* favour of an option value, so the viewer label keeps matching the editor's field name.
|
||||
*/
|
||||
private boolean looksAutoGenerated(String value) {
|
||||
if (value == null) return true;
|
||||
|
||||
RegexPatternUtils patterns = RegexPatternUtils.getInstance();
|
||||
String simplified = patterns.getPunctuationPattern().matcher(value).replaceAll(" ").trim();
|
||||
if (simplified.isEmpty()) return true;
|
||||
|
||||
String nospaces = WHITESPACE_PATTERN.matcher(simplified).replaceAll("");
|
||||
if (nospaces.length() >= 32 && HEX_UUID_PATTERN.matcher(nospaces).matches()) return true;
|
||||
|
||||
return patterns.getPattern("^field(\\s*\\d+)?$", Pattern.CASE_INSENSITIVE)
|
||||
.matcher(simplified)
|
||||
.matches()
|
||||
|| patterns.getSimpleFormFieldPattern().matcher(simplified).matches()
|
||||
|| patterns.getOptionalTNumericPattern().matcher(simplified).matches();
|
||||
}
|
||||
|
||||
private String humanizeName(String name) {
|
||||
if (name == null) return null;
|
||||
|
||||
|
||||
@@ -295,6 +295,42 @@ class FormUtilsEditingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractFormFields_prefersFieldNameOverFirstOptionForChoiceLabel() throws IOException {
|
||||
// A radio group named "Choice" with options Yes/No must be labelled
|
||||
// "Choice" (its name), not "Yes" (its first option). Otherwise the label
|
||||
// shown in the viewer disagrees with the field name shown in the editor.
|
||||
byte[] saved;
|
||||
try (PDDocument document = new PDDocument()) {
|
||||
setupForm(document, PDRectangle.A4);
|
||||
FormUtils.addNewFields(
|
||||
document,
|
||||
List.of(
|
||||
newField(
|
||||
"radio",
|
||||
"Choice",
|
||||
60,
|
||||
700,
|
||||
16,
|
||||
16,
|
||||
List.of("Yes", "No"),
|
||||
null,
|
||||
null)));
|
||||
saved = save(document);
|
||||
}
|
||||
|
||||
try (PDDocument reloaded = Loader.loadPDF(saved)) {
|
||||
FormUtils.FormFieldInfo choice =
|
||||
FormUtils.extractFormFields(reloaded).stream()
|
||||
.filter(f -> "Choice".equals(f.name()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
assertNotNull(choice, "radio field should be extracted");
|
||||
assertEquals(
|
||||
"Choice", choice.label(), "field name should win over the first option value");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNewFields_createsCombTextField() throws IOException {
|
||||
byte[] saved;
|
||||
|
||||
Reference in New Issue
Block a user