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 G
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 7.5 0 m
"
+ " 7.5 4.1423 4.1423 7.5 0 7.5 c
"
+ " -4.1423 7.5 -7.5 4.1423 -7.5 0 c
"
+ " -7.5 -4.1423 -4.1423 -7.5 0 -7.5 c
"
+ " 4.1423 -7.5 7.5 -4.1423 7.5 0 c
"
+ " s
"
+ "Q";
String onNString = "0 G
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 7.5 0 m
"
+ " 7.5 4.1423 4.1423 7.5 0 7.5 c
"
+ " -4.1423 7.5 -7.5 4.1423 -7.5 0 c
"
+ " -7.5 -4.1423 -4.1423 -7.5 0 -7.5 c
"
+ " 4.1423 -7.5 7.5 -4.1423 7.5 0 c
"
+ " s
"
+ "Q
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 3.5 0 m
"
+ " 3.5 1.9331 1.9331 3.5 0 3.5 c
"
+ " -1.9331 3.5 -3.5 1.9331 -3.5 0 c
"
+ " -3.5 -1.9331 -1.9331 -3.5 0 -3.5 c
"
+ " 1.9331 -3.5 3.5 -1.9331 3.5 0 c
"
+ " f
"
+ "Q";
String offDString = "0.749023 g
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 8 0 m
"
+ " 8 4.4185 4.4185 8 0 8 c
"
+ " -4.4185 8 -8 4.4185 -8 0 c
"
+ " -8 -4.4185 -4.4185 -8 0 -8 c
"
+ " 4.4185 -8 8 -4.4185 8 0 c
"
+ " f
"
+ "Q
"
+ "0 G
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 7.5 0 m
"
+ " 7.5 4.1423 4.1423 7.5 0 7.5 c
"
+ " -4.1423 7.5 -7.5 4.1423 -7.5 0 c
"
+ " -7.5 -4.1423 -4.1423 -7.5 0 -7.5 c
"
+ " 4.1423 -7.5 7.5 -4.1423 7.5 0 c
"
+ " s
"
+ "Q";
String onDString = "0.749023 g
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 8 0 m
"
+ " 8 4.4185 4.4185 8 0 8 c
"
+ " -4.4185 8 -8 4.4185 -8 0 c
"
+ " -8 -4.4185 -4.4185 -8 0 -8 c
"
+ " 4.4185 -8 8 -4.4185 8 0 c
"
+ " f
"
+ "Q
"
+ "0 G
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 7.5 0 m
"
+ " 7.5 4.1423 4.1423 7.5 0 7.5 c
"
+ " -4.1423 7.5 -7.5 4.1423 -7.5 0 c
"
+ " -7.5 -4.1423 -4.1423 -7.5 0 -7.5 c
"
+ " 4.1423 -7.5 7.5 -4.1423 7.5 0 c
"
+ " s
"
+ "Q
"
+ "0 g
"
+ "q
"
+ " 1 0 0 1 8 8 cm
"
+ " 3.5 0 m
"
+ " 3.5 1.9331 1.9331 3.5 0 3.5 c
"
+ " -1.9331 3.5 -3.5 1.9331 -3.5 0 c
"
+ " -3.5 -1.9331 -1.9331 -3.5 0 -3.5 c
"
+ " 1.9331 -3.5 3.5 -1.9331 3.5 0 c
"
+ " f
"
+ "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.