Author: msahyoun
Date: Tue Mar 24 16:57:48 2026
New Revision: 1932510
Log:
PDFBOX-6178: set AS and V entry resolving matching COSName
Modified:
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
Modified:
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
==============================================================================
---
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
Tue Mar 24 16:39:56 2026 (r1932509)
+++
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java
Tue Mar 24 16:57:48 2026 (r1932510)
@@ -321,6 +321,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.
Modified:
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
==============================================================================
---
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
Tue Mar 24 16:39:56 2026 (r1932509)
+++
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDButton.java
Tue Mar 24 16:57:48 2026 (r1932510)
@@ -335,11 +335,11 @@ public abstract class PDButton extends P
COSName value = getCOSObject().getCOSName(COSName.V);
if (((COSDictionary)
appearanceEntry.getCOSObject()).containsKey(value))
{
- widget.setAppearanceState(value.getName());
+ widget.setAppearanceState(value);
}
else
{
- widget.setAppearanceState(COSName.Off.getName());
+ widget.setAppearanceState(COSName.Off);
}
}
}
@@ -430,24 +430,78 @@ public abstract class PDButton extends P
private void updateByValue(String value) throws IOException
{
- getCOSObject().setName(COSName.V, value);
+ // Find the matching appearance key from the first widget that has it
+ COSName matchingKey = null;
+
// update the appearance state (AS)
for (PDAnnotationWidget widget : getWidgets())
{
- if (widget.getAppearance() == null)
+ PDAppearanceDictionary appearance = widget.getAppearance();
+ if (appearance == null)
{
continue;
}
PDAppearanceEntry appearanceEntry =
widget.getAppearance().getNormalAppearance();
- if (((COSDictionary)
appearanceEntry.getCOSObject()).containsKey(value))
+ COSDictionary appearanceDict = (COSDictionary)
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) throws IOException
Modified:
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
==============================================================================
---
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
Tue Mar 24 16:39:56 2026 (r1932509)
+++
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/form/TestRadioButtons.java
Tue Mar 24 16:57:48 2026 (r1932510)
@@ -16,6 +16,7 @@
*/
package org.apache.pdfbox.pdmodel.interactive.form;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -419,5 +420,136 @@ public class TestRadioButtons
{
IOUtils.closeQuietly(testPdf);
}
- }
+ }
+
+ /**
+ * 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
+ public void testPDFBox6178NonAsciiRadioButtonValue() throws IOException
+ {
+ PDDocument document = null;
+ 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
+ {
+ document = PDDocument.load(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("V entry should not be null after setValue", vEntry);
+
+ // Check that the bytes contain 0xE4 (ISO-8859-1 ä) not 0xC3 0xA4
(UTF-8)
+ byte[] vBytes = vEntry.getBytes();
+
+ assertFalse("V entry should not contain UTF-8 encoded ä (0xC3
0xA4)",
+ containsSequence(vBytes, new byte[]{(byte) 0xC3, (byte)
0xA4}));
+ assertTrue("V entry should contain ISO-8859-1 encoded ä (0xE4)",
+ containsSequence(vBytes, new byte[]{(byte) 0xE4}));
+
+ // Verify AS entry preserves encoding
+ COSName asEntry = (COSName)
field.getWidgets().get(0).getCOSObject()
+ .getDictionaryObject(COSName.AS);
+ assertNotNull("AS entry should not be null after setValue",
asEntry);
+
+ // Check that the AS bytes also preserve ISO-8859-1 encoding
+ byte[] asBytes = asEntry.getBytes();
+
+ assertFalse("AS entry should not contain UTF-8 encoded ä (0xC3
0xA4)",
+ containsSequence(asBytes, new byte[]{(byte) 0xC3, (byte)
0xA4}));
+ assertTrue("AS entry should contain ISO-8859-1 encoded ä (0xE4)",
+ containsSequence(asBytes, new byte[]{(byte) 0xE4}));
+
+ document.save(baos);
+ }
+ finally
+ {
+ IOUtils.closeQuietly(document);
+ }
+
+ // Reload and verify entries are still correct
+ try
+ {
+ document = PDDocument.load(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("V entry should not be null after reload", vEntry);
+
+ byte[] vBytes = vEntry.getBytes();
+ assertFalse("V entry should still not contain UTF-8 ä after
reload",
+ containsSequence(vBytes, new byte[]{(byte) 0xC3, (byte)
0xA4}));
+ assertTrue("V entry should still contain ISO-8859-1 ä after
reload",
+ containsSequence(vBytes, new byte[]{(byte) 0xE4}));
+
+ // Verify AS entry after reload
+ COSName asEntry = (COSName)
field.getWidgets().get(0).getCOSObject()
+ .getDictionaryObject(COSName.AS);
+ assertNotNull("AS entry should not be null after reload", asEntry);
+
+ byte[] asBytes = asEntry.getBytes();
+ assertFalse("AS entry should still not contain UTF-8 ä after
reload",
+ containsSequence(asBytes, new byte[]{(byte) 0xC3, (byte)
0xA4}));
+ assertTrue("AS entry should still contain ISO-8859-1 ä after
reload",
+ containsSequence(asBytes, new byte[]{(byte) 0xE4}));
+ }
+ finally
+ {
+ IOUtils.closeQuietly(document);
+ }
+ }
+
+ /**
+ * 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;
+ }
}
\ No newline at end of file