Author: msahyoun
Date: Tue Mar 24 16:07:10 2026
New Revision: 1932507
Log:
PDFBOX-6178: set AS and V entry resolving matching COSName
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
==============================================================================
---
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
Tue Mar 24 13:34:45 2026 (r1932506)
+++
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
Tue Mar 24 16:07:10 2026 (r1932507)
@@ -337,6 +337,20 @@ public abstract class PDAnnotation imple
}
/**
+ * This will set the annotations appearance state name.
+ *
+ * <p>Note that the PDF specification defines the AS entry as a name, but
some PDFs use a string.
+ * This method will write a name, which is correct and should be preferred
but may cause issues
+ * with some viewers if the PDF being edited already uses a string.</p>
+ *
+ * @param as The COSName of the appearance stream.
+ */
+ public void setAppearanceState(COSName as)
+ {
+ getCOSObject().setItem(COSName.AS, as);
+ }
+
+ /**
* This will get the appearance dictionary associated with this
annotation. This may return null.
*
* @return This annotations appearance.
@@ -384,7 +398,7 @@ public abstract class PDAnnotation imple
}
else
{
-// PDAppearanceStream extends PDFormXObject, but does not reference the
resource cache
+ // PDAppearanceStream extends PDFormXObject, but does not
reference the resource cache
return normalAppearance.getAppearanceStream();
}
}
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
==============================================================================
---
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
Tue Mar 24 13:34:45 2026 (r1932506)
+++
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
Tue Mar 24 16:07:10 2026 (r1932507)
@@ -296,11 +296,11 @@ public abstract class PDButton extends P
COSName value = getCOSObject().getCOSName(COSName.V);
if (appearanceEntry.getCOSObject().containsKey(value))
{
- widget.setAppearanceState(value.getName());
+ widget.setAppearanceState(value);
}
else
{
- widget.setAppearanceState(COSName.Off.getName());
+ widget.setAppearanceState(COSName.Off);
}
}
}
@@ -390,8 +390,10 @@ public abstract class PDButton extends P
private void updateByValue(String value)
{
- getCOSObject().setName(COSName.V, value);
- // update the appearance state (AS)
+ // Find the matching appearance key from the first widget that has it
+ COSName matchingKey = null;
+
+ // update the appearance state (AS) for each widget
for (PDAnnotationWidget widget : getWidgets())
{
PDAppearanceDictionary appearance = widget.getAppearance();
@@ -400,15 +402,65 @@ public abstract class PDButton extends P
continue;
}
PDAppearanceEntry appearanceEntry =
appearance.getNormalAppearance();
- if (appearanceEntry.getCOSObject().containsKey(value))
+ COSDictionary appearanceDict = appearanceEntry.getCOSObject();
+
+ // Find the matching appearance key by searching through the
actual keys
+ // and comparing their decoded names. This handles encoding
differences:
+ // the appearance key might be ISO-8859-1 encoded (e.g.
/m#e4nnlich for "männlich")
+ // while the value String is UTF-8.
+ COSName widgetMatchingKey =
findMatchingAppearanceKey(appearanceDict, value);
+
+ // Save the first matching key to use for the V entry
+ if (widgetMatchingKey != null && matchingKey == null)
{
- widget.setAppearanceState(value);
+ matchingKey = widgetMatchingKey;
+ }
+
+ if (widgetMatchingKey != null)
+ {
+ // Use the exact COSName from the appearance dictionary to
preserve encoding
+ widget.setAppearanceState(widgetMatchingKey);
}
else
{
- widget.setAppearanceState(COSName.Off.getName());
+ // Fall back to Off if no match found for this widget
+ widget.setAppearanceState(COSName.Off);
+ }
+ }
+
+ // Set the V entry once using the first matching key found
+ if (matchingKey != null)
+ {
+ getCOSObject().setItem(COSName.V, matchingKey);
+ }
+ else
+ {
+ // Fall back to UTF-8 encoding if no match found in any widget
+ getCOSObject().setName(COSName.V, value);
+ }
+ }
+
+ /**
+ * Find the appearance dictionary key that matches the given value String.
+ * This method handles encoding differences - the value might be UTF-8
while
+ * appearance keys in the PDF could be ISO-8859-1 or other encodings.
+ *
+ * @param appearanceDict the appearance dictionary with keys to search
+ * @param value the value String to match against (typically UTF-8)
+ * @return the matching COSName key, or null if no match found
+ */
+ private COSName findMatchingAppearanceKey(COSDictionary appearanceDict,
String value)
+ {
+ // Search all keys in the appearance dictionary and compare their
decoded names
+ // COSName.getName() uses UTF-8 decoding with ISO-8859-1 fallback for
non-UTF-8 bytes
+ for (COSName key : appearanceDict.keySet())
+ {
+ if (value.equals(key.getName()))
+ {
+ return key;
}
}
+ return null;
}
private void updateByOption(String value)
Modified:
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
==============================================================================
---
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
Tue Mar 24 13:34:45 2026 (r1932506)
+++
pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
Tue Mar 24 16:07:10 2026 (r1932507)
@@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Asse
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -366,4 +367,124 @@ class TestRadioButtons
assertEquals(1, field.getSelectedIndex());
}
}
+
+ /**
+ * PDFBOX-6178: Ensure that RadioButton values with non-ASCII characters
preserve encoding.
+ * When setting a RadioButton value to "männlich", both V and AS entries
should preserve
+ * the original byte encoding from the appearance dictionary (0xE4 for ä
in ISO-8859-1,
+ * not 0xC3 0xA4 from UTF-8).
+ *
+ * @throws IOException
+ */
+ @Test
+ void testPDFBox6178NonAsciiRadioButtonValue() throws IOException
+ {
+ File pdfFile = new File("target/pdfs/PDFBOX-6178.pdf");
+ if (!pdfFile.exists())
+ {
+ return; // Skip test if PDF not available
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ // Load document, set value, and save to memory
+ try (PDDocument document = Loader.loadPDF(pdfFile))
+ {
+ PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
+ PDField field = acroForm.getField("Geschlecht");
+
+ field.setValue("männlich");
+
+ // Verify V entry preserves encoding - should have 0xE4 byte for ä
(ISO-8859-1)
+ COSName vEntry = (COSName)
field.getCOSObject().getDictionaryObject(COSName.V);
+ assertNotNull(vEntry, "V entry should not be null after setValue");
+
+ // Check that the bytes contain 0xE4 (ISO-8859-1 ä) not 0xC3 0xA4
(UTF-8)
+ byte[] vBytes = vEntry.getBytes();
+
+ assertFalse(containsSequence(vBytes, new byte[]{(byte) 0xC3,
(byte) 0xA4}),
+ "V entry should not contain UTF-8 encoded ä (0xC3 0xA4)");
+ assertTrue(containsSequence(vBytes, new byte[]{(byte) 0xE4}),
+ "V entry should contain ISO-8859-1 encoded ä (0xE4)");
+
+ // Verify AS entry preserves encoding
+ COSName asEntry = (COSName)
field.getWidgets().get(0).getCOSObject()
+ .getDictionaryObject(COSName.AS);
+ assertNotNull(asEntry, "AS entry should not be null after
setValue");
+
+ // Check that the AS bytes also preserve ISO-8859-1 encoding
+ byte[] asBytes = asEntry.getBytes();
+
+ assertFalse(containsSequence(asBytes, new byte[]{(byte) 0xC3,
(byte) 0xA4}),
+ "AS entry should not contain UTF-8 encoded ä (0xC3 0xA4)");
+ assertTrue(containsSequence(asBytes, new byte[]{(byte) 0xE4}),
+ "AS entry should contain ISO-8859-1 encoded ä (0xE4)");
+
+ document.save(baos);
+ }
+
+ // Reload and verify entries are still correct
+ try (PDDocument document = Loader.loadPDF(baos.toByteArray()))
+ {
+ PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
+ PDField field = acroForm.getField("Geschlecht");
+
+ // Verify V entry after reload
+ COSName vEntry = (COSName)
field.getCOSObject().getDictionaryObject(COSName.V);
+ assertNotNull(vEntry, "V entry should not be null after reload");
+
+ byte[] vBytes = vEntry.getBytes();
+ assertFalse(containsSequence(vBytes, new byte[]{(byte) 0xC3,
(byte) 0xA4}),
+ "V entry should still not contain UTF-8 ä after reload");
+ assertTrue(containsSequence(vBytes, new byte[]{(byte) 0xE4}),
+ "V entry should still contain ISO-8859-1 ä after reload");
+
+ // Verify AS entry after reload
+ COSName asEntry = (COSName)
field.getWidgets().get(0).getCOSObject()
+ .getDictionaryObject(COSName.AS);
+ assertNotNull(asEntry, "AS entry should not be null after reload");
+
+ byte[] asBytes = asEntry.getBytes();
+ assertFalse(containsSequence(asBytes, new byte[]{(byte) 0xC3,
(byte) 0xA4}),
+ "AS entry should still not contain UTF-8 ä after reload");
+ assertTrue(containsSequence(asBytes, new byte[]{(byte) 0xE4}),
+ "AS entry should still contain ISO-8859-1 ä after reload");
+ }
+ }
+
+ /**
+ * Helper method to check if a byte sequence contains a particular
sub-sequence.
+ *
+ * @param haystack the bytes to search in
+ * @param needle the bytes to search for
+ * @return true if needle is found in haystack, false otherwise
+ */
+ private boolean containsSequence(byte[] haystack, byte[] needle)
+ {
+ if (needle.length == 0)
+ {
+ return true;
+ }
+ if (needle.length > haystack.length)
+ {
+ return false;
+ }
+ for (int i = 0; i <= haystack.length - needle.length; i++)
+ {
+ boolean match = true;
+ for (int j = 0; j < needle.length; j++)
+ {
+ if (haystack[i + j] != needle[j])
+ {
+ match = false;
+ break;
+ }
+ }
+ if (match)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
}