0

I want a pdf to be signed by multiple users sequentially. They can also add text or an image of their initials while they sign the document digitally. I am using pdfbox version 2.0.25.

I have used the code from here in edit 3 of the question for adding the text while also signing the document. And below is the code for adding a signature field to the pdf.

try (FileOutputStream fos = new FileOutputStream(signedFile)) {
            PDSignature signature = new PDSignature();
            PDDocument doc = PDDocument.load(inputFile);
            for (DocumentSigningObject signingObject : documentSigningObjectList) {
                SignatureOptions signatureOptions = new SignatureOptions();
                SignerOptionEntity signerOption = signingObject.getSignerOptionEntity();

                PDRectangle rect = createSignatureRectangle(doc, signingObject.getPlaceholderEntity());
                signatureOptions.setPreferredSignatureSize(SignatureOptions.DEFAULT_SIGNATURE_SIZE * 2);
                signatureOptions.setPage(signingObject.getSignatureComponents().getPage() - 1);
                signatureOptions
                        .setVisualSignature(createVisualSignatureTemplate(doc, rect,
                                signerOption, signingObject.getSignatureComponents().getPage(),
                                signingObject.getSignatureComponents().getText()));

                signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
                signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
                signature.setName(name);

                if (Objects.nonNull(signerOption) && doLocationExists(signerOption)) {
                    signature.setLocation(getFormattedLocationValue(signerOption));
                }

                signature.setSignDate(Calendar.getInstance());
                signature.setReason(Objects.nonNull(signerOption)
                        && StringUtils.isNotBlank(signerOption.getSignatureNote())
                        ? signerOption.getSignatureNote() : "");
                doc.addSignature(signature, signatureOptions);
                ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
                MessageDigest digest = MessageDigest.getInstance("SHA-256");
                byte[] hashBytes = digest.digest(IOUtils.toByteArray(externalSigning.getContent()));
                base64Hash = Base64.getEncoder().encodeToString(hashBytes);
                externalSigning.setSignature(new byte[0]);
                offset = signature.getByteRange()[1] + 1;
                IOUtils.closeQuietly(signatureOptions);
            }

Method createSignatureVisualTemplate()

try (PDDocument doc = new PDDocument()) {
            int pageNum = pageNo - 1;
            PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
            doc.addPage(page);
            PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
            if (acroForm == null) {
                acroForm = new PDAcroForm(doc);
            }
            doc.getDocumentCatalog().setAcroForm(acroForm);
            acroForm.setSignaturesExist(true);
            acroForm.setAppendOnly(true);
            acroForm.getCOSObject().setDirect(true);

            PDSignatureField signatureField = new PDSignatureField(acroForm);
            List<PDField> acroFormFields = acroForm.getFields();
            acroFormFields.add(signatureField);
            PDAnnotationWidget widget = signatureField.getWidgets().get(0);
            widget.setRectangle(rect);

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

            // 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);
                }
                cs.saveGraphicsState();
                //Use the signature image as received, without any modification
                if (Objects.nonNull(signatureOption) && Objects.nonNull(signatureOption.getSignatureImage())) {
                    PDImageXObject img = PDImageXObject.createFromByteArray(doc,
                            getBase64ImageByteData(signatureOption), null);
                    cs.drawImage(img, 0, 0, rect.getWidth(), rect.getHeight());

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

After the first user signs the document the following message is displayed when the pdf is opened in AdobeAcrobatReader.

Pdf signed by first user for the fisrt time

Now the second user signs the already signed pdf and adds his initials as well. Now the message displayed on the reader is : Pdf signed second time by different user

The texts are added before the signature fields are added to the document but it says that the document was updated after it was signed. Although both the signatures are still valid, can someone explain the message and a way so that only validity of the signatures is displayed.

EDIT-1

I am using a different approach now, I am adding all the textFields for the initials before signing the document and populating them when the user comes to sign the document.

public Blob addPlaceholders(Blob documentBlob, List<DocumentSigningObject> placeholderList,
                String userId, boolean arePlaceholdersAdded)
            throws IOException, SQLException {
        //return if placeholders are already added or there are no placeholders for initials
        if (arePlaceholdersAdded || placeholderList.isEmpty()) {
            return documentBlob;
        }
        byte[] bytes = documentBlob.getBytes(1L, (int) documentBlob.length());
        File placeholdersAddedFile = new File("temp", getTempFileName("placeholdersAdded_"));
        //ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Set<COSDictionary> objectsToWrite = new HashSet<>();
        Blob placeholderAddedBlob;
        try (FileOutputStream fos = new FileOutputStream(placeholdersAddedFile)) {
            PDDocument pdDocument = PDDocument.load(bytes);
            for (DocumentSigningObject signingObject : placeholderList) {
                PDAcroForm form = pdDocument.getDocumentCatalog().getAcroForm();
                if (form == null) {
                    form = new PDAcroForm(pdDocument);
                }
                pdDocument.getDocumentCatalog().setAcroForm(form);
                PDFont font = PDType1Font.HELVETICA;
                PDResources resources = new PDResources();
                resources.put(COSName.getPDFName("Helv"), font);
                form.setDefaultResources(resources);
                PDTextField textField = new PDTextField(form);
                String fieldName = SigningPlaceholderType.INITIALS.name() + signingObject.getPlaceholderId();
                textField.setPartialName(fieldName);
                String defaultAppearance = "/Helv 12 Tf 0 0 0 rg";
                textField.setDefaultAppearance(defaultAppearance);
                textField.setReadOnly(true);
                form.getFields().add(textField);
                PDAnnotationWidget widget = textField.getWidgets().get(0);
                PDRectangle rect = createSignatureRectangle(pdDocument, signingObject);
                int pageNum = signingObject.getPage() - 1;
                PDPage page = pdDocument.getPage(pageNum);
                widget.setRectangle(rect);
                widget.setPage(page);
                PDAppearanceCharacteristicsDictionary fieldAppearance =
                        new PDAppearanceCharacteristicsDictionary(new COSDictionary());
                fieldAppearance.setBorderColour(new PDColor(new float[]{}, PDDeviceRGB.INSTANCE));
                widget.setAppearanceCharacteristics(fieldAppearance);
                widget.setPrinted(true);
                page.getAnnotations().add(widget);
                page.getCOSObject().setNeedToBeUpdated(true);
                form.getFields().add(textField);
                pdDocument.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true);
            }
            pdDocument.saveIncremental(fos, objectsToWrite);
            try (FileInputStream fis = new FileInputStream(placeholdersAddedFile)) {
                placeholderAddedBlob = new javax.sql.rowset.serial.SerialBlob(fis.readAllBytes());
            }
            Files.delete(placeholdersAddedFile.toPath());
            log.debug("temp signed file {} deleted successfully", placeholdersAddedFile.getName());
        }
        return placeholderAddedBlob;
    }

Now all the blank fields are already in the document and we just fetch and add text to them along with signing the document.

public Blob addInitials(Blob placeholdersAddedFile, List<DocumentSigningObject> initialsInfoList, String userId)
            throws IOException, SQLException {

        if (initialsInfoList.isEmpty()) {
            return placeholdersAddedFile;
        }
        byte[] bytes = placeholdersAddedFile.getBytes(1L, (int) placeholdersAddedFile.length());
        File initialedFile = new File("temp", getTempFileName("initialsAdded_"));
        Blob initialedDoucmentBlob;
        try (FileOutputStream fos = new FileOutputStream(initialedFile)) {
            PDDocument doc = PDDocument.load(bytes);
            Set<COSDictionary> objectsToWrite = new HashSet<>();
            for (DocumentSigningObject initialsInfoObject : initialsInfoList) {
                String initialsImage = initialsInfoObject.getInitialsImage();
                String initialsText = initialsInfoObject.getText();

                //fetch the field created for initials from the document and populate the values
                PDDocumentCatalog docCatalog = doc.getDocumentCatalog();
                PDAcroForm acroForm = docCatalog.getAcroForm();
                if (acroForm != null) {
                    String fieldName = SigningPlaceholderType.INITIALS.name()
                            + initialsInfoObject.getPlaceholderId();
                    PDField field = acroForm.getField(fieldName);
                    if (StringUtils.isNotBlank(initialsText)) {
                        field.setValue(initialsText);
                        field.setReadOnly(true);
                    } else if (StringUtils.isNotBlank(initialsImage)) {
                        List<PDAnnotationWidget> widgets = field.getWidgets();
                        if (widgets != null && widgets.size() > 0) {

                            Blob initialsImageBlob = DocumentManagerUtils.convertBase64StringToBlob(initialsImage);
                            PDImageXObject ximage = PDImageXObject.createFromByteArray(doc,
                                    Base64.getDecoder().decode(initialsImageBlob.getBytes(1L,
                                            (int) initialsImageBlob.length())), null);
                            float imageScaleRatio = (float) ximage.getHeight() / (float) ximage.getWidth();

                            PDRectangle fieldRectangle = getFieldArea(field);
                            float height = fieldRectangle.getHeight();
                            float width = height / imageScaleRatio;
                            float x = fieldRectangle.getLowerLeftX();
                            float y = fieldRectangle.getLowerLeftY();

                            PDAppearanceStream pdAppearanceStream = new PDAppearanceStream(doc);
                            pdAppearanceStream.setResources(new PDResources());
                            try (PDPageContentStream pdPageContentStream = new PDPageContentStream(doc,
                                    pdAppearanceStream)) {
                                pdPageContentStream.drawImage(ximage, x, y, width, height);
                            }
                            pdAppearanceStream.setBBox(new PDRectangle(x, y, width, height));
                            PDAnnotationWidget annotationWidget = widgets.get(0);
                            PDAppearanceDictionary pdAppearanceDictionary = annotationWidget.getAppearance();
                            if (pdAppearanceDictionary == null) {
                                pdAppearanceDictionary = new PDAppearanceDictionary();
                                annotationWidget.setAppearance(pdAppearanceDictionary);
                            }
                            pdAppearanceDictionary.setNormalAppearance(pdAppearanceStream);
                        }
                    }
                    objectsToWrite.add(field.getCOSObject());
                    acroForm.getCOSObject().setNeedToBeUpdated(true);
                    field.getCOSObject().setNeedToBeUpdated(true);
                    doc.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true);
                }
            }
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            doc.saveIncremental(fos, objectsToWrite);
            try (FileInputStream fis = new FileInputStream(initialedFile)) {
                initialedDoucmentBlob = new javax.sql.rowset.serial.SerialBlob(fis.readAllBytes());
            }
            Files.delete(initialedFile.toPath());
            log.debug("temp signed file {} deleted successfully", initialedFile.getName());
        }
        return initialedDoucmentBlob;
    }

The adding signature field code is already shown above. using this approach the document can be signed multiple times and all of them are valid but the issue of multiple references to objects after revisions can be seen. The pdfs are below.

Pdf signed first Pdf signed second

Rawatr
  • 1
  • 2
  • [pdf signed first](https://drive.google.com/file/d/11h9aQLcjGpJKdJTwLNEl3ax4hbkxI6iw/view?usp=share_link) [pdf-signed-second](https://drive.google.com/file/d/1w5wQktc16yK-8ou_EutPvWXwukvcnWIN/view?usp=share_link) – Rawatr Apr 17 '23 at 03:49
  • Adobe is being misleading IMHO. The stamp was added with a separate incremental saving before signing (maybe you weren't aware of that). The text for the first signature says "Diese Revision wurde nicht geändert" i.e. Adobe admits that the signed version wasn't altered for the first file. – Tilman Hausherr Apr 17 '23 at 08:41
  • Does your edit represent the solution to your problem? Then you had better put that into an answer (which you eventually can mark as _accepted answer_). – mkl Apr 28 '23 at 07:17
  • @mkl *Does your edit represent the solution to your problem?* The same issue of mutiple references to an object after revisions is still there in the pdfs shared in the edit. I have shared the code of how I am adding initials and signatures for mutiple users. I am still not sure about the root cause of the issue. But the signatures are always valid. – Rawatr Apr 30 '23 at 15:31

1 Answers1

0

First of all, the Stamp annotation on page 2 indeed is added after the first signature. Thus, the message displayed after the second signature is correct.

Furthermore, there are errors in your PDFs. Such errors can cause the most interesting effects during signature validation. Probably even that "annotation modified" on the file signed once.

In detail:

Your file has multiple Revisions:

  • The first revision (first 3028 bytes, object numbers up to 10)

    Here the two pages have no annotations.

  • The second revision (first 4492 bytes, object numbers up to 16)

    Here the first page gets a Stamp annotation.

    This revision has a multiple new objects for page 1 and cross reference issues.

  • The third revision (first 44969 bytes, object numbers up to 22)

    Here the first signature is applied with a signature widget on the second page.

  • The fourth revision (first 366531 bytes, object numbers up to 28)

    Here validation related information are added to the DSS of the PDF.

(Only in the copy signed twice:)

  • The fifth revision (first 368156 bytes, object number up to 34)

    Here the second page gets a Stamp annotation.

    This revision has a multiple new objects for page 2 and cross reference issues.

  • The sixth revision (first 407980 bytes, object numbers up to 39)

    Here the second signature is applied with a signature widget on the first page.

  • The seventh revision (all 729587 bytes, object numbers up to 44)

    Here validation related information are added to the DSS of the PDF.

The issues in revision 2 and 5:

In each case the updated page dictionary of the page in question is there twice in the respective revision and there also are two entries for it in the cross references.

In revision 2 object 4 (page 1) has entries at offsets 3030 and 3443:

xref
0 2
0000000000 65535 f
0000003198 00000 n
3 2
0000003263 00000 n
0000003030 00000 n
4 1
0000003442 00000 n
11 6
0000003326 00000 n
0000003610 00000 n
0000003642 00000 n
0000003926 00000 n
0000003961 00000 n
0000003994 00000 n

In revision 5 object 6 (page 2) has entries at offsets 366533 and 367089:

xref
0 2
0000000000 65535 f
0000366708 00000 n
3 1
0000366910 00000 n
6 1
0000366533 00000 n
6 1
0000367089 00000 n
29 6
0000366973 00000 n
0000367264 00000 n
0000367296 00000 n
0000367582 00000 n
0000367617 00000 n
0000367650 00000 n

This is strictly invalid.

mkl
  • 90,588
  • 15
  • 125
  • 265
  • thanks for the detailed analysis. I am not sure how multiple references for the objects are being created, if you could provide some insights into what is going wrong causing this issue? I have also tried a different approach to add all the text fields for initials of the user before signing the document starts and populate the field value with text when the user signs the document. Here is the [pdf] (https://drive.google.com/file/d/1pj3i83UPCa4M9NOzTI4bpVfQtMPBFwlB/view?usp=share_link) it seems to have the same issue as the previous ones – Rawatr Apr 18 '23 at 09:13
  • @Rawatr *"if you could provide some insights into what is going wrong causing this issue?"* - Please share the code that causes the issue. As detailed above, problematic revisions are those in which the stamps are added. In your question you say you used the code from EDIT 3 of [this question](https://stackoverflow.com/q/41467415/1729265) for that task. [I just tested that](https://github.com/mkl-public/testarea-pdfbox2/blob/master/src/test/java/mkl/testarea/pdfbox2/annotate/AddAnnotationIncrementally.java#L54), both with PDFBox 2.0.28 and your 2.0.25, but could not reproduce the issue... – mkl Apr 19 '23 at 15:00