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

Reply via email to