Skip to content
Advertisement

Radiobutton display problems with PDFBox

I used the code from the answer from this question to create my radiobuttons: How to Create a Radio Button Group with PDFBox 2.0

After I created my PDF and tried to read the (programatically) selected value from it, this code worked fine:

    PDDocumentCatalog catalog = doc.getDocumentCatalog();
    PDAcroForm form = catalog.getAcroForm();
    List<PDField> fields = form.getFields();

    for(PDField field: fields) {
        Object value = field.getValueAsString();
        String name = field.getFullyQualifiedName();
        if (field instanceof PDRadioButton) {
            // value is correct and field is instance of PDRadioButton works too
        }

    }

When I open the PDF in Acrobat Reader DC, make changes and save it again the code doesn’t work anymore. There is no instance of PDRadioButton anymore and the value is always an empty string. enter image description here

When I open the PDF in Acrobat Touch it doesn’t even display properly. enter image description here

(When I open the version that was previously edited by Acrobat Reader DC, Acrobat Touch can display it correctly)

Any suggestions what may be wrong with the code?

Here is a minimal example that behaves the same:

package test;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;


public class WriterTest {
    public static void main(String[] args) {
        try {
            PDDocument document = new PDDocument();
            PDPage page = new PDPage(PDRectangle.A4);

            document.addPage(page);

            PDAcroForm acroForm = new PDAcroForm(document);
            acroForm.setNeedAppearances(true);
            acroForm.setXFA(null);
            document.getDocumentCatalog().setAcroForm(acroForm);

            PDFont font = PDType1Font.HELVETICA;

            PDResources res = new PDResources();
            COSName fontName = res.add(font);
            acroForm.setDefaultResources(res);
            acroForm.setDefaultAppearance('/' + fontName.getName() + " 10 Tf 0 g");

            PDPageContentStream contents = new PDPageContentStream(document, page);

            List<String> options = Arrays.asList("a", "b", "c");
            PDRadioButton radioButton = new PDRadioButton(acroForm);
            radioButton.setPartialName("RadioButtonParent");
            radioButton.setExportValues(options);
            radioButton.getCOSObject().setName(COSName.DV, options.get(1));

            List<PDAnnotationWidget> widgets = new ArrayList<>();
            for (int i = 0; i < options.size(); i++) {
                PDRadioButton subRadioButtons = new PDRadioButton(acroForm);
                subRadioButtons.setPartialName("RadioButton");

                PDAppearanceCharacteristicsDictionary fieldAppearance = new PDAppearanceCharacteristicsDictionary(new COSDictionary());
                fieldAppearance.setBorderColour(new PDColor(new float[] { 0, 0, 0 }, PDDeviceRGB.INSTANCE));

                PDAnnotationWidget widget = subRadioButtons.getWidgets().get(0);
                widget.setRectangle(new PDRectangle(30, 811 - i * (21), 16, 16));
                widget.setAppearanceCharacteristics(fieldAppearance);

                widgets.add(widget);
                page.getAnnotations().add(widget);

                contents.beginText();
                contents.setFont(font, 10);
                contents.newLineAtOffset(56, 811 - i * (21) + 4);
                contents.showText(options.get(i));
                contents.endText();
            }
            radioButton.setWidgets(widgets);
            acroForm.getFields().add(radioButton);

            contents.close();
            try (FileOutputStream output = new FileOutputStream("test.pdf")) {
                document.save(output);
            }
            document.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Advertisement

Answer

Your code displays the top of the field tree. The javadoc of getFields() warns about this:

/**
 * This will return all of the documents root fields.
 * 
 * A field might have children that are fields (non-terminal field) or does not
 * have children which are fields (terminal fields).
 * 
 * The fields within an AcroForm are organized in a tree structure. The documents root fields 
 * might either be terminal fields, non-terminal fields or a mixture of both. Non-terminal fields
 * mark branches which contents can be retrieved using {@link PDNonTerminalField#getChildren()}.
 * 
 * @return A list of the documents root fields.
 * 
 */
public List<PDField> getFields()

To get all the fields (this includes non terminal fields), do this:

PDDocumentCatalog catalog = doc.getDocumentCatalog();
PDAcroForm form = catalog.getAcroForm();
Iterator<PDField> fieldIterator = form.getFieldIterator();
while (fieldIterator.hasNext())
{
    PDField field = fieldIterator.next();
    // ... do stuff ...
}

Then your radio button appears.

However there are still another problem. The choice is returned as “a”, “b” or “Choice1” instead of “c”.

I was able to fix that by adding this code segment before adding the widget:

PDAppearanceDictionary appearance = new PDAppearanceDictionary();
COSDictionary dict = new COSDictionary();
dict.setItem(COSName.getPDFName("Off"), new COSDictionary());
dict.setItem(COSName.getPDFName(options.get(i)), new COSDictionary());
PDAppearanceEntry appearanceEntry = new PDAppearanceEntry(dict);
appearance.setNormalAppearance(appearanceEntry);
widget.setAppearance(appearance);

It adds empty appearances for “Off” and for the on-Option to each button.

Update 17.1.2017:

Here’s source code to generate radio buttons with appearance streams:

PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);

document.addPage(page);

PDAcroForm acroForm = new PDAcroForm(document);

// not needed, we have appearance streams
//acroForm.setNeedAppearances(true);

acroForm.setXFA(null);
document.getDocumentCatalog().setAcroForm(acroForm);

PDFont font = PDType1Font.HELVETICA;

PDResources res = new PDResources();
COSName fontName = res.add(font);
acroForm.setDefaultResources(res);
acroForm.setDefaultAppearance('/' + fontName.getName() + " 10 Tf 0 g");

PDPageContentStream contents = new PDPageContentStream(document, page);

List<String> options = Arrays.asList("a", "b", "c");
PDRadioButton radioButton = new PDRadioButton(acroForm);
radioButton.setPartialName("RadioButtonParent");
// removed per advice of Maruan Sahyoun, setValue didn't work anymore
//radioButton.setExportValues(options);
radioButton.getCOSObject().setName(COSName.DV, options.get(1));
radioButton.setFieldFlags(49152);
int on = 1;

List<PDAnnotationWidget> widgets = new ArrayList<>();
for (int i = 0; i < options.size(); i++)
{
    PDAppearanceCharacteristicsDictionary fieldAppearance = new PDAppearanceCharacteristicsDictionary(new COSDictionary());
    fieldAppearance.setBorderColour(new PDColor(new float[] { 0, 0, 0 }, PDDeviceRGB.INSTANCE));
    PDAnnotationWidget widget = new PDAnnotationWidget();
    widget.setRectangle(new PDRectangle(30, 811 - i * (21), 16, 16));
    widget.setAppearanceCharacteristics(fieldAppearance);
    widget.setAnnotationFlags(4);
    widget.setPage(page);
    widget.setParent(radioButton);

    String offNString = "0 Gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  7.5 0 mn"
            + "  7.5 4.1423 4.1423 7.5 0 7.5 cn"
            + "  -4.1423 7.5 -7.5 4.1423 -7.5 0 cn"
            + "  -7.5 -4.1423 -4.1423 -7.5 0 -7.5 cn"
            + "  4.1423 -7.5 7.5 -4.1423 7.5 0 cn"
            + "  sn"
            + "Q";
    String onNString = "0 Gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  7.5 0 mn"
            + "  7.5 4.1423 4.1423 7.5 0 7.5 cn"
            + "  -4.1423 7.5 -7.5 4.1423 -7.5 0 cn"
            + "  -7.5 -4.1423 -4.1423 -7.5 0 -7.5 cn"
            + "  4.1423 -7.5 7.5 -4.1423 7.5 0 cn"
            + "  sn"
            + "Qn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  3.5 0 mn"
            + "  3.5 1.9331 1.9331 3.5 0 3.5 cn"
            + "  -1.9331 3.5 -3.5 1.9331 -3.5 0 cn"
            + "  -3.5 -1.9331 -1.9331 -3.5 0 -3.5 cn"
            + "  1.9331 -3.5 3.5 -1.9331 3.5 0 cn"
            + "  fn"
            + "Q";
    String offDString = "0.749023 gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  8 0 mn"
            + "  8 4.4185 4.4185 8 0 8 cn"
            + "  -4.4185 8 -8 4.4185 -8 0 cn"
            + "  -8 -4.4185 -4.4185 -8 0 -8 cn"
            + "  4.4185 -8 8 -4.4185 8 0 cn"
            + "  fn"
            + "Qn"
            + "0 Gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  7.5 0 mn"
            + "  7.5 4.1423 4.1423 7.5 0 7.5 cn"
            + "  -4.1423 7.5 -7.5 4.1423 -7.5 0 cn"
            + "  -7.5 -4.1423 -4.1423 -7.5 0 -7.5 cn"
            + "  4.1423 -7.5 7.5 -4.1423 7.5 0 cn"
            + "  sn"
            + "Q";
    String onDString = "0.749023 gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  8 0 mn"
            + "  8 4.4185 4.4185 8 0 8 cn"
            + "  -4.4185 8 -8 4.4185 -8 0 cn"
            + "  -8 -4.4185 -4.4185 -8 0 -8 cn"
            + "  4.4185 -8 8 -4.4185 8 0 cn"
            + "  fn"
            + "Qn"
            + "0 Gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  7.5 0 mn"
            + "  7.5 4.1423 4.1423 7.5 0 7.5 cn"
            + "  -4.1423 7.5 -7.5 4.1423 -7.5 0 cn"
            + "  -7.5 -4.1423 -4.1423 -7.5 0 -7.5 cn"
            + "  4.1423 -7.5 7.5 -4.1423 7.5 0 cn"
            + "  sn"
            + "Qn"
            + "0 gn"
            + "qn"
            + "  1 0 0 1 8 8 cmn"
            + "  3.5 0 mn"
            + "  3.5 1.9331 1.9331 3.5 0 3.5 cn"
            + "  -1.9331 3.5 -3.5 1.9331 -3.5 0 cn"
            + "  -3.5 -1.9331 -1.9331 -3.5 0 -3.5 cn"
            + "  1.9331 -3.5 3.5 -1.9331 3.5 0 cn"
            + "  fn"
            + "Q";
    COSDictionary apNDict = new COSDictionary();
    COSStream offNStream = new COSStream();
    offNStream.setItem(COSName.BBOX, new PDRectangle(16, 16));
    offNStream.setItem(COSName.FORMTYPE, COSInteger.ONE);
    offNStream.setItem(COSName.TYPE, COSName.XOBJECT);
    offNStream.setItem(COSName.SUBTYPE, COSName.FORM);
    OutputStream os = offNStream.createOutputStream(COSName.FLATE_DECODE);
    os.write(offNString.getBytes());
    os.close();
    apNDict.setItem(COSName.Off, offNStream);

    COSStream onNStream = new COSStream();
    onNStream.setItem(COSName.BBOX, new PDRectangle(16, 16));
    onNStream.setItem(COSName.FORMTYPE, COSInteger.ONE);
    onNStream.setItem(COSName.TYPE, COSName.XOBJECT);
    onNStream.setItem(COSName.SUBTYPE, COSName.FORM);
    os = onNStream.createOutputStream(COSName.FLATE_DECODE);
    os.write(onNString.getBytes());
    os.close();
    apNDict.setItem(options.get(i), onNStream);

    COSDictionary apDDict = new COSDictionary();
    COSStream offDStream = new COSStream();
    offDStream.setItem(COSName.BBOX, new PDRectangle(16, 16));
    offDStream.setItem(COSName.FORMTYPE, COSInteger.ONE);
    offDStream.setItem(COSName.TYPE, COSName.XOBJECT);
    offDStream.setItem(COSName.SUBTYPE, COSName.FORM);
    os = offDStream.createOutputStream(COSName.FLATE_DECODE);
    os.write(offDString.getBytes());
    os.close();
    apDDict.setItem(COSName.Off, offDStream);

    COSStream onDStream = new COSStream();
    onDStream.setItem(COSName.BBOX, new PDRectangle(16, 16));
    onDStream.setItem(COSName.FORMTYPE, COSInteger.ONE);
    onDStream.setItem(COSName.TYPE, COSName.XOBJECT);
    onDStream.setItem(COSName.SUBTYPE, COSName.FORM);
    os = onDStream.createOutputStream(COSName.FLATE_DECODE);
    os.write(onDString.getBytes());
    os.close();
    apDDict.setItem(options.get(i), onDStream);

    PDAppearanceDictionary appearance = new PDAppearanceDictionary();
    PDAppearanceEntry appearanceNEntry = new PDAppearanceEntry(apNDict);
    appearance.setNormalAppearance(appearanceNEntry);
    PDAppearanceEntry appearanceDEntry = new PDAppearanceEntry(apDDict);
    appearance.setDownAppearance(appearanceDEntry);

    widget.setAppearance(appearance);

    widget.setAppearanceState(i == on ? options.get(i) : "Off");

    widgets.add(widget);
    page.getAnnotations().add(widget);

    contents.beginText();
    contents.setFont(font, 10);
    contents.newLineAtOffset(56, 811 - i * (21) + 4);
    contents.showText(options.get(i));
    contents.endText();
}
radioButton.setWidgets(widgets);
acroForm.getFields().add(radioButton);

contents.close();
try (FileOutputStream output = new FileOutputStream("test.pdf"))
{
    document.save(output);
}
document.close();

If you want Adobe to generate the appearance streams (that’s the “gibberish” in the code), call setNeedAppearances(true) and remove the line widget.setAppearance(appearance);. If you open the file with Adobe and save it, the appearance streams will be generated, and that is where I got these from. You can see these with PDFDebugger if you look at the annotations, then AP and go down from there.

That’s also the strategy to use if you want to know the appearance stream content for bigger buttons.

Some time in the future PDFBox will generate the appearance streams for buttons. There is some math involved, see here or in the trunk source code in PDCircleAppearanceHandler.

Advertisement