Trying to create a PDF fillable form using java 8 and pdfbox 2.0.30 . For each topping I want a checkbox with a box drawn around it and the topping. Seems pretty simple.
Code executes and viewing the resulting toppingsForm.pdf in a browser shows
import java.io.*;
import java.util.*;
import java.awt.Color;
import java.awt.geom.Rectangle2D;
import org.apache.pdfbox.*;
import org.apache.pdfbox.util.*;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
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.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.interactive.*;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.pdmodel.interactive.form.*;
import org.apache.pdfbox.text.*;
import org.apache.pdfbox.contentstream.*;
import org.apache.pdfbox.pdfparser.*;
import org.apache.pdfbox.io.*;
public class Toppings {
private static float getLineWidth( PDAnnotationWidget widget) {
PDBorderStyleDictionary bs = widget.getBorderStyle();
if( bs != null) {
return bs.getWidth();
}
return 1;
}
private static void drawRect( PDDocument document, PDPage page, PDRectangle rect) {
try {
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
float ll_x = rect.getLowerLeftX();
float ll_y = rect.getLowerLeftY();
float height = rect.getHeight();
float length = rect.getWidth();
contentStream.addRect( ll_x, ll_y, height, length);
contentStream.setLineWidth(1);
contentStream.setNonStrokingColor(Color.WHITE);
contentStream.setStrokingColor(Color.BLACK);
contentStream.stroke();
contentStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// Generate Form Labels
private static void addText( PDDocument document, PDPage page, String myText, float x, float y, boolean bold) {
try {
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA, 6);
if( bold) {
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 6);
}
contentStream.newLineAtOffset(x, y);
contentStream.showText(myText);
contentStream.endText();
contentStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void addCheckbox( PDDocument pdfDoc, PDAcroForm acroForm, PDPage page, String name, float x, float y) {
try {
PDCheckBox checkbox = new PDCheckBox(acroForm);
checkbox.setPartialName( name);
PDAppearanceCharacteristicsDictionary appearanceCharacteristics = new PDAppearanceCharacteristicsDictionary(new COSDictionary());
appearanceCharacteristics.setBorderColour(new PDColor(new float[] { 1, 0, 0 }, PDDeviceRGB.INSTANCE));
appearanceCharacteristics.setBackground( new PDColor(new float[]{0, 1, 0.3f}, PDDeviceRGB.INSTANCE));
appearanceCharacteristics.setNormalCaption("4");
PDBorderStyleDictionary borderStyleDictionary = new PDBorderStyleDictionary();
borderStyleDictionary.setWidth(1);
borderStyleDictionary.setStyle(PDBorderStyleDictionary.STYLE_SOLID);
PDAnnotationWidget widget = new PDAnnotationWidget();
widget.setRectangle( new PDRectangle( x, y, 16, 16) );
drawRect( pdfDoc, page, widget.getRectangle()); // comment out this line and toppings appear
widget.setAnnotationFlags(4);
widget.setBorderStyle(borderStyleDictionary);
widget.setPage( page);
widget.setParent( checkbox);
List<PDAnnotationWidget> widgets = new ArrayList<>();
widgets.add(widget);
page.getAnnotations().add(widget);
checkbox.setWidgets(widgets);
acroForm.getFields().add(checkbox);
addText( pdfDoc, page, name, x + 20, y + 6, false); // comment out this line and checkbox outlines appear
} catch (IOException e) {
e.printStackTrace();
}
}
private static void addToppings( PDDocument pdfDoc, PDAcroForm acroForm, PDPage page, float x, float y) throws IOException {
List<String> options = Arrays.asList("pepperoni","sausage","ham"
,"chicken","canadian bacon","mushrooms"
,"pineapple","onions","green pepper"
,"red pepper","black olives","green olives");
for( int i = 0; i < 12; i++ ) {
addCheckbox( pdfDoc, acroForm, page, options.get(i), x, y);
y = y - 20;
}
}
public static void main(String[] args) {
try {
PDDocument pdfDoc = new PDDocument(); // outfile
PDPage page = new PDPage();
pdfDoc.addPage(page);
PDPageContentStream contentStream = new PDPageContentStream( pdfDoc, page);
PDAcroForm acroForm = new PDAcroForm( pdfDoc);
float x = 20;
float y = 500;
addToppings( pdfDoc, acroForm, page, x, y);
contentStream.close();
pdfDoc.save("C:/Users/MainUser/toppingsForm.pdf");
pdfDoc.close();
} catch ( IOException e) {
e.getMessage();
}
}
}
My first time using pdfBox to create forms, am usually filling them in. Maybe there's some subtle sizing/overlap issue I'm bumping in to. Or a better way altogether, like setting a visible border on the checkbox itself.
TIA,
Still-learning Steve
1- add pdfDoc.getDocumentCatalog().setAcroForm(acroForm);
near the end
2- keep drawRect()
but remove contentStream.setNonStrokingColor(Color.WHITE);
because this means your text is in white.
3- add this below checkbox.setWidgets(widgets);
widget.setAppearanceCharacteristics(appearanceCharacteristics);
PDAppearanceDictionary ap = new PDAppearanceDictionary();
widget.setAppearance(ap);
PDAppearanceEntry normalAppearance = ap.getNormalAppearance();
COSDictionary normalAppearanceDict = (COSDictionary) normalAppearance.getCOSObject();
normalAppearanceDict.setItem(COSName.Off, createAppearanceStream(pdfDoc, widget, false));
normalAppearanceDict.setItem(COSName.YES, createAppearanceStream(pdfDoc, widget, true));
4- add this:
private static PDAppearanceStream createAppearanceStream(
final PDDocument document, PDAnnotationWidget widget, boolean on) throws IOException
{
PDRectangle rect = widget.getRectangle();
PDAppearanceCharacteristicsDictionary appearanceCharacteristics;
PDAppearanceStream yesAP = new PDAppearanceStream(document);
yesAP.setBBox(new PDRectangle(rect.getWidth(), rect.getHeight()));
yesAP.setResources(new PDResources());
PDPageContentStream yesAPCS = new PDPageContentStream(document, yesAP);
appearanceCharacteristics = widget.getAppearanceCharacteristics();
PDColor backgroundColor = appearanceCharacteristics.getBackground();
PDColor borderColor = appearanceCharacteristics.getBorderColour();
float lineWidth = getLineWidth(widget);
yesAPCS.setLineWidth(lineWidth); // border style (dash) ignored
yesAPCS.setNonStrokingColor(backgroundColor);
yesAPCS.addRect(0, 0, rect.getWidth(), rect.getHeight());
yesAPCS.fill();
yesAPCS.setStrokingColor(borderColor);
yesAPCS.addRect(lineWidth / 2, lineWidth / 2, rect.getWidth() - lineWidth, rect.getHeight() - lineWidth);
yesAPCS.stroke();
if (!on)
{
yesAPCS.close();
return yesAP;
}
yesAPCS.addRect(lineWidth, lineWidth, rect.getWidth() - lineWidth * 2, rect.getHeight() - lineWidth * 2);
yesAPCS.clip();
String normalCaption = appearanceCharacteristics.getNormalCaption();
if (normalCaption == null)
{
normalCaption = "4"; // Adobe behaviour
}
if ("8".equals(normalCaption))
{
// Adobe paints a cross instead of using the Zapf Dingbats cross symbol
yesAPCS.setStrokingColor(0f);
yesAPCS.moveTo(lineWidth * 2, rect.getHeight() - lineWidth * 2);
yesAPCS.lineTo(rect.getWidth() - lineWidth * 2, lineWidth * 2);
yesAPCS.moveTo(rect.getWidth() - lineWidth * 2, rect.getHeight() - lineWidth * 2);
yesAPCS.lineTo(lineWidth * 2, lineWidth * 2);
yesAPCS.stroke();
}
else
{
Rectangle2D bounds = new Rectangle2D.Float();
String unicode = null;
// ZapfDingbats font may be missing or substituted, let's use AFM resources instead.
AFMParser parser = new AFMParser(PDType1Font.class.getResourceAsStream(
"/org/apache/pdfbox/resources/afm/ZapfDingbats.afm"));
FontMetrics metric = parser.parse();
for (CharMetric cm : metric.getCharMetrics())
{
// The caption is not unicode, but the Zapf Dingbats code in the PDF.
// Assume that only the first character is used.
if (normalCaption.codePointAt(0) == cm.getCharacterCode())
{
BoundingBox bb = cm.getBoundingBox();
bounds = new Rectangle2D.Float(bb.getLowerLeftX(), bb.getLowerLeftY(),
bb.getWidth(), bb.getHeight());
unicode = PDType1Font.ZAPF_DINGBATS.getGlyphList().toUnicode(cm.getName());
break;
}
}
if (bounds.isEmpty())
{
throw new IOException("Bounds rectangle for chosen glyph is empty");
}
float size = (float) Math.min(bounds.getWidth(), bounds.getHeight()) / 1000;
// assume that checkmark has square size
// the calculations approximate what Adobe is doing, i.e. put the glyph in the middle
float fontSize = (rect.getWidth() - lineWidth * 2) / size * 0.6666f;
float xOffset = (float) (rect.getWidth() - (bounds.getWidth()) / 1000 * fontSize) / 2;
xOffset -= bounds.getX() / 1000 * fontSize;
float yOffset = (float) (rect.getHeight() - (bounds.getHeight()) / 1000 * fontSize) / 2;
yOffset -= bounds.getY() / 1000 * fontSize;
yesAPCS.setNonStrokingColor(0f);
yesAPCS.beginText();
yesAPCS.setFont(PDType1Font.ZAPF_DINGBATS, fontSize);
yesAPCS.newLineAtOffset(xOffset, yOffset);
yesAPCS.showText(unicode);
yesAPCS.endText();
}
yesAPCS.close();
return yesAP;
}
(3) and (4) are from the CreateCheckBox example; I suspect you used parts of it.
5- (update) looking at the example, it turns out you don't need drawRect()
at all. Instead, add checkbox.unCheck();
after the line checkbox.setWidgets(widgets);
. This makes sure that the checkbox in a defined state.