javapdfboxpdfbox-layout

Trying to draw box around checkbox and display text using pdfbox


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


Solution

  • 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.