One of the most common features of a document signing application is the ability to add the visual representation (or plainly put, an image) of the user’s signature to a pdf. This article describes an approach where a base64 encoded image is used as the visual representation of the user’s signature based on the following example code from the official pdfbox repo: https://github.com/apache/pdfbox/blob/trunk/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature2.java

The main differences being:

  • We do not generate the image dynamically, but rather just use a static base64 encoded png. So, it’s up to you how to generate that image.
  • We use a coordinate system that starts from bottom left as it is a more natural way to place the signature – it’s much more common to find the signatures at the bottom of the page than at the top of the page.

Full example

package com.eideasy.pdf;

import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.io.IOUtils;
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.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.util.Matrix;

import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.*;
import java.util.*;
import java.util.List;

public class VisualSigner {
    public void addVisibleSignatureToPdf() throws IOException {
        String pdfInBase64 = "JVBERi0xLjQKJdPr....jEwODg5CiUlRU9G";
        String signatureImageInBase64 = "iVBORw0KGgoAAAA...gAAAAASUVORK5CYII=";
        long signatureTime = 1692617329341L;

        PDDocument document = PDDocument.load(Base64.getDecoder().decode(pdfInBase64));

        PDSignature signature = new PDSignature();

        signature.setType(COSName.getPDFName("Sig"));
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);

        // The signing date, needed for a valid signature
        final Calendar cal = Calendar.getInstance();
        final Date signingDate = new Date(signatureTime);
        cal.setTime(signingDate);
        signature.setSignDate(cal);
        SignatureOptions options = new SignatureOptions();

        // Enough room for signature, timestamp and OCSP for baseline-LT profile.
        options.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE);

        // The code for visible signature starts here
        int pageNum = 0; // Page numbering starts from zero.
        options.setPage(pageNum);
        Rectangle2D humanRect = new Rectangle2D.Float(
            20,
            10,
            200,
            100
        );

        PDRectangle rect = createSignatureRectangle(document, humanRect);
        options.setVisualSignature(createVisualSignatureTemplate(
            document,
            pageNum,
            rect,
            signatureImageInBase64
        ));

        document.addSignature(signature, options);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        document.saveIncrementalForExternalSigning(baos);

        FileOutputStream outputStream = new FileOutputStream("prepared.pdf");
        document.save(outputStream);

        outputStream.close();
        document.close();
        IOUtils.closeQuietly(options);
    }

    private PDRectangle createSignatureRectangle(PDDocument doc, Rectangle2D humanRect) {
        float x = (float) humanRect.getX();
        float y = (float) humanRect.getY();
        float width = (float) humanRect.getWidth();
        float height = (float) humanRect.getHeight();
        PDPage page = doc.getPage(0);
        PDRectangle pageRect = page.getCropBox();
        PDRectangle rect = new PDRectangle();

        // Signature image should be at the same position regardless of page rotation.
        // Coordinates start from bottom left.
        switch (page.getRotation()) {
            case 90:
                rect.setLowerLeftX(pageRect.getWidth() - y - height);
                rect.setUpperRightX(pageRect.getWidth() - y);
                rect.setLowerLeftY(x);
                rect.setUpperRightY(x + width);
                break;
            case 180:
                rect.setLowerLeftX(pageRect.getWidth() - x - width);
                rect.setUpperRightX(pageRect.getWidth() - x);
                rect.setLowerLeftY(pageRect.getHeight() - y - height);
                rect.setUpperRightY(pageRect.getHeight() - y);
                break;
            case 270:
                rect.setLowerLeftX(y);
                rect.setUpperRightX(y + height);
                rect.setLowerLeftY(pageRect.getHeight() - x - width);
                rect.setUpperRightY(pageRect.getHeight() - x);
                break;
            case 0:
            default:
                rect.setLowerLeftX(x);
                rect.setUpperRightX(x + width);
                rect.setLowerLeftY(y);
                rect.setUpperRightY(y + height);
                break;
        }

        return rect;
    }

    // Create a template PDF document with empty signature and return it as a stream.
    private InputStream createVisualSignatureTemplate(
            PDDocument srcDoc,
            int pageNum,
            PDRectangle rect,
            String imageInBase64
    ) throws IOException {
        try (PDDocument doc = new PDDocument()) {
            PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
            doc.addPage(page);
            PDAcroForm acroForm = new PDAcroForm(doc);
            doc.getDocumentCatalog().setAcroForm(acroForm);
            PDSignatureField signatureField = new PDSignatureField(acroForm);
            PDAnnotationWidget widget = signatureField.getWidgets().get(0);
            List<PDField> acroFormFields = acroForm.getFields();
            acroForm.setSignaturesExist(true);
            acroForm.setAppendOnly(true);
            acroForm.getCOSObject().setDirect(true);
            acroFormFields.add(signatureField);

            widget.setRectangle(rect);

            // from PDVisualSigBuilder.createHolderForm()
            PDStream stream = new PDStream(doc);
            PDFormXObject form = new PDFormXObject(stream);
            PDResources res = new PDResources();
            form.setResources(res);
            form.setFormType(1);
            PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
            Matrix initialScale = null;
            int pageRotation = srcDoc.getPage(0).getRotation();
            switch (pageRotation) {
                case 90:
                    form.setMatrix(AffineTransform.getQuadrantRotateInstance(1));
                    initialScale = Matrix.getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());
                    break;
                case 180:
                    form.setMatrix(AffineTransform.getQuadrantRotateInstance(2));
                    break;
                case 270:
                    form.setMatrix(AffineTransform.getQuadrantRotateInstance(3));
                    initialScale = Matrix.getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());
                    break;
                default:
                    break;
            }
            form.setBBox(bbox);

            // From PDVisualSigBuilder.createAppearanceDictionary()
            PDAppearanceDictionary appearance = new PDAppearanceDictionary();
            appearance.getCOSObject().setDirect(true);
            PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
            appearance.setNormalAppearance(appearanceStream);
            widget.setAppearance(appearance);

            try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
                if (initialScale != null) {
                    cs.transform(initialScale);
                }

                if (imageInBase64 != null) {
                    byte[] image = Base64.getDecoder().decode(imageInBase64);
                    cs.saveGraphicsState();
                    PDImageXObject img = PDImageXObject.createFromByteArray(doc, image, "signature.png");

                    float imageWidth = bbox.getWidth();
                    float imageHeight = bbox.getHeight();
                    if (pageRotation == 90 || pageRotation == 270) {
                        imageWidth = bbox.getHeight();
                        imageHeight = bbox.getWidth();
                    }
                    cs.drawImage(img, 0, 0, imageWidth, imageHeight);
                    cs.restoreGraphicsState();
                }

            }

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            doc.save(baos);
            return new ByteArrayInputStream(baos.toByteArray());
        }
    }
}

Good to know

In createSignatureRectangle method, we set the signature rectangle coordinates using the

setLowerLeftX, setUpperRightX etc. methods. So what do these methods do exactly?

In short, they allow to set the position of the rectangle’s lower left corner and the upper right corner. See the following diagram:

GDPR Badge BVCER ISO 27001 eIDAS eID Easy Google for Startups