javapdfpdfbox

Multiple signs using PDFBox 2.0.12 Java


I was trying to add multiple signatures in a single PDF on stamper. I am able to add multiple stampers. In my case on one, I was getting the error

at least one signature is invalid. Also need to make valid all signature.

How can I want to add multiple valid signs in a single PDF document?

In the image, only one sign is valid and other signs are invalid, so what am I doing wrong?

My code snapshot is below:

public void getSignOnPdf(Map<Integer, byte[]> PdfSigneture1, List<Long> documentIds, List<String> calTimeStamp,
        String originalPdfReadServerPath, String tickImagePath, int serverTime, int pageNumberToInsertStamp,
        String name, String location, String reasonForSign, int xCo_ordinates, int yCo_ordinates,
        int signatureWidth, int signatureHeight, String pdfPassword, String internal_outputFinalPdfPath)
        throws Exception {
    String pdfReadServerPath = null;
    String l_slash = new String();
    String originalPDFPath = new String(originalPdfReadServerPath.trim());

    boolean isCorrectPDFOutputPath = false;
    String aspOutputPdfServerPath = null;
    synchronized (this) {
        if ((internal_outputFinalPdfPath != null) && (!internal_outputFinalPdfPath.trim().isEmpty())) {
            System.out.println("[" + EsignCommonFuntion.generateTimeStampForLog()
                    + "] :1-->  outputFinalPdfPath is: " + internal_outputFinalPdfPath);
            if (!(new File(internal_outputFinalPdfPath)).isFile()) {
                isCorrectPDFOutputPath = true;
                aspOutputPdfServerPath = internal_outputFinalPdfPath;
            } else {
                System.out.println("1--> Please provide directory path for outputFinalPdfPath: "
                        .concat(String.valueOf(internal_outputFinalPdfPath)));
            }
        } else {
            System.out.println(" 1--> outputFinalPdfPath is empty or null: "
                    .concat(String.valueOf(internal_outputFinalPdfPath)));
        }
    }
    boolean isPasswordPresent = false;
    String pdfPasswordForEncryption;
    synchronized (this) {
        if ((pdfPassword != null) && (!pdfPassword.trim().isEmpty())) {
            pdfPasswordForEncryption = pdfPassword.trim();
            isPasswordPresent = true;
        } else {
            pdfPasswordForEncryption = null;
        }
        String pdfOriginalName = (new File(originalPDFPath)).getName();
        String pdfAbsolutePath = originalPDFPath.substring(0, originalPDFPath.lastIndexOf(l_slash));
        if (isPasswordPresent) {
            pdfAbsolutePath = getEncryptedPdfName(originalPDFPath, pdfAbsolutePath + l_slash,
                    pdfPasswordForEncryption, pdfOriginalName);
            pdfReadServerPath = new String(pdfAbsolutePath);
        } else {
            pdfReadServerPath = originalPDFPath;
        }
    }
    ArrayList<String> unSignedFilesList = new ArrayList<String>();

    Map<Integer, byte[]> l_PdfSigneture = PdfSigneture1;

    int actual_pageNumForStamp = 1;

    String pdfFileName = (new File(pdfReadServerPath)).getName();

    FileOutputStream fos = null;

    String nameToShowInSignature = name;
    String locationToShowInSignature = location;
    String reasonForSignatureSign = reasonForSign;

    PDDocument documentFinal = null;
    try {
        pdfReadServerPath = pdfReadServerPath.substring(0, pdfReadServerPath.lastIndexOf(l_slash));
        System.out.println("inside getSignOnMethod pdfAbsolutePath:".concat(String.valueOf(pdfReadServerPath)));
        unSignedFilesList.add(pdfFileName);
        System.out.println("inside getSignOnMethod pdfFileName:".concat(String.valueOf(pdfFileName)));

        String PDFpath = pdfReadServerPath + l_slash + (String) (unSignedFilesList).get(0);

        System.out.println("Inside for PDFpath: ".concat(String.valueOf(PDFpath)));

        String finalOutputPdfName = ((String) (unSignedFilesList).get(0)).substring(0,
                ((String) (unSignedFilesList).get(0)).lastIndexOf(".")) + "_signedFinal.pdf";

        File outFile2 = null;

        if (isCorrectPDFOutputPath) {
            System.out.println("if condition Final signed PDF ouptut Path: " + aspOutputPdfServerPath + l_slash
                    + finalOutputPdfName);
            outFile2 = new File(aspOutputPdfServerPath + l_slash + finalOutputPdfName);
            fos = new FileOutputStream(outFile2);
        } else {
            outFile2 = new File(pdfReadServerPath + l_slash + outFile2);
            fos = new FileOutputStream(outFile2);
        }

        documentFinal = PDDocument.load(new File(PDFpath));

        for (int i = 1; i < 4; i++) {
            FileInputStream image2 = new FileInputStream(tickImagePath);

            PDSignature pdsignature = new PDSignature();
            pdsignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            pdsignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);

            Calendar cal = GregorianCalendar.getInstance();
            SimpleDateFormat l_simpleDateFormater = new SimpleDateFormat("yyyyMMdd_HHmmss");
            String timeStamp = (String) calTimeStamp.get(i - 1);

            try {
                cal.setTime(l_simpleDateFormater.parse(timeStamp));
            } catch (ParseException ex) {
                ex.printStackTrace();
            }

            cal.add(12, serverTime);
            pdsignature.setSignDate(cal);
            documentFinal.setDocumentId((Long) documentIds.get(i - 1));

            String dateToShowInSignature = cal.getTime().toString();

            Float saveIncrementalObj1 = null;
            saveIncrementalObj1 = new Float((float) xCo_ordinates, (float) yCo_ordinates, (float) signatureWidth,
                    (float) signatureHeight);

            PDRectangle rect = getPDRectangle(documentFinal, saveIncrementalObj1, i);
            PDVisibleSignDesigner visibleSig;
            (visibleSig = new PDVisibleSignDesigner(documentFinal, image2, i)).xAxis(xCo_ordinates)
                    .yAxis(yCo_ordinates).zoom(-95.0F).signatureFieldName("signature");

            PDVisibleSigProperties visibleSignatureProp = new PDVisibleSigProperties();

            visibleSignatureProp.signerName("name").signerLocation("location").signatureReason("Security")
                    .preferredSize(0).page(i - 1).visualSignEnabled(true).setPdVisibleSignature(visibleSig)
                    .buildSignature();
            try {
                PdfSigneture = new TreeMap<>();
                // PdfSigneture.clear();
                PdfSigneture = l_PdfSigneture;

                if (visibleSignatureProp.isVisualSignEnabled()) {
                    this.options = new SignatureOptions();
                    this.options.setVisualSignature(visibleSignatureProp);
                    this.options.setPage(visibleSignatureProp.getPage());
                    this.options.setVisualSignature(
                            getInputStream(documentFinal, i, rect, tickImagePath, nameToShowInSignature,
                                    locationToShowInSignature, dateToShowInSignature, reasonForSignatureSign));
                    documentFinal.addSignature(pdsignature, this, this.options);
                } else {
                    documentFinal.addSignature(pdsignature, this);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        synchronized (this) {
            SaveIncrementalSignObject saveIncrementalSignObject;
            (saveIncrementalSignObject = new SaveIncrementalSignObject()).setFos(fos);
            saveIncrementalSignObject.setPDDocumentFromFile(documentFinal);

            saveIncrementalForSign(saveIncrementalSignObject);
        }
    } catch (Exception localException2) {
        System.out.println("Insidemethod -- Exception block" + localException2.getMessage());
        return;
    } finally {
        fos.flush();
        if (fos != null) {
            fos.close();
        }
        documentFinal.close();
    }
}

public static synchronized void saveIncrementalForSign(SaveIncrementalSignObject p_SaveIncrementalObj) {
    PDDocument documentFinal = null;
    try {
        (documentFinal = p_SaveIncrementalObj.getPDDocumentFromFile())
                .saveIncremental(p_SaveIncrementalObj.getFos());
    } catch (Exception e) {
        e.printStackTrace();
        try {
        //                documentFinal.close();
            return;
        } catch (Exception eX) {
            eX.printStackTrace();
            return;
        }
    }
}

Solution

  • In a comment you clarified what you want to achieve:

    I tried to applying one signature to multiple place.

    As discussed in the first section below this is not what your code does: your code attempts to apply multiple signatures to one place each in a single revision which is impossible as also explained there.

    Applying a single signature to multiple places in a single revision, on the other hand, is not desired by the PDF specification team and some approaches to implement this have been made invalid by the specification, but it is possible as explained in the second section below.

    Your approach, and why it cannot work

    You appear to try to apply multiple signatures in one pass:

    if (isPasswordPresent) {
        documentFinal = PDDocument.load(new File(PDFpath), pdfPasswordForEncryption);
    } else {
        documentFinal = PDDocument.load(new File(PDFpath));
    }
    
    for (int i = 1; i < 4; i++) {
        FileInputStream image2 = new FileInputStream(tickImagePath);
    
        PDSignature pdsignature = new PDSignature();
    
        [...]
    
        try {
            [...]
    
            if (visibleSignatureProp.isVisualSignEnabled()) {
                [...]
                documentFinal.addSignature(pdsignature, this, this.options);
            } else {
                documentFinal.addSignature(pdsignature, this);
            }
        } catch (Exception e) {
            System.out.println("Inside getSignOnPdf sub exception block at addSignature:" + e + "error :" + e.getMessage());
            e.printStackTrace();
        }
    }
    
    synchronized (this) {
        [...]
        saveIncrementalForSign(saveIncrementalSignObject);
    }
    

    This cannot work.

    In PDFs multiple signatures are applied one after the other in separate PDF revisions, not all in parallel in the same revision:

    image

    You can find some backgrounds in this answer and the documents referenced from there.

    Thus, in pseudo code what you have to do instead is:

    for (int i = 1; i < 4; i++) {
        load current version of the PDF;
        apply the i'th signature;
        save and sign as new current version of the PDF;
    }
    

    The method name PDDocument.addSignature might be a little misleading here as it might be assumed to imply that multiple signatures may be added. This is not the case; all signatures will be created as signature fields with their widgets but only the field of the last added PDSignature will actually be signed, so only this last added signature field will actually have a sensible value.

    @Tilman - there probably should be a test in PDDocument.addSignature throwing an exception if a signature already has been added since loading the document.

    A discussion of your actual task

    The path of PDF objects from a signature visualization on a PDF page to the actual signature (the CMS signature container in case of CMS based subfilters) is not immediate. Instead we have

    For the implementation of your actual task,

    applying one signature to multiple places,

    therefore, there appear to be a number of options to get from multiple pages with signature appearances to the single signature container:

    1. All pages with signature visualizations pointing to the same single widget annotation of the single signature field with the value dictionary containing the signature container.
    2. Each page with signature visualizations pointing to their own widget, but all widgets belonging to the same single signature field with the value dictionary containing the signature container.
    3. Each page with signature visualizations pointing to their own widget, each widget belonging to a separate signature field, but all of them pointing to the same value dictionary containing the signature container.

    Let's now look at the PDF specification ISO 32000-2. First of all it warns against having single signatures with multiple visualizations:

    The location of a signature within a document can have a bearing on its legal meaning. [...]

    If more than one location is associated with a signature, the meaning can become ambiguous.

    (ISO 32000-2, section 12.7.5.5 "Signature fields")

    Consequentially, the specification attempts to forbid single signatures with multiple visualizations:

    A given annotation dictionary shall be referenced from the Annots array of only one page.

    (ISO 32000-2, section 12.5.2 "Annotation dictionaries")

    This forbids option 1 above.

    signature fields shall never refer to more than one annotation

    (ISO 32000-2, section 12.7.5.5 "Signature fields")

    This forbids option 2.

    Apparently, though, option 3 is not explicitly forbidden. For generic form fields value object sharing is even explicitly allowed as the form field value is inheritable!

    Thus, strictly speaking creating signatures with multiple visualizations is possible using option 3.

    Please be aware, though, that it clearly was not intended by the PDF specification team to allow them, it most likely was an oversight. Thus, you have to reckon that some upcoming corrigenda to the specification will eventually forbid option 3, too.

    If you want to try nonetheless, it should be possible to tweak or patch PDFBox to create single signatures with multiple visualizations using the approach of option 3.

    It has already proven possible for e.g. iText, cf. this answer.

    Furthermore, the sample document you shared makes use of this option.

    A proof of concept

    As it turns out, it is pretty easy to create a multi-visualization PDF signature using PDFBox along the lines of option 3. In particular it is easier than doing this with iText, cf. the answer referenced above, because the signature value dictionary here is an object one creates and handles oneself while in iText it is created under the hood and just in time.

    All one has to do is to create one PDSignature object and generate one signature with it normally (using PDDocument.addSignature) and then add as many other signature fields as one wants, setting the signature value properties of those fields to the single PDSignature object create at the start.

    E.g. you can use a method like this to add additional signature fields:

    void addSignatureField(PDDocument pdDocument, PDPage pdPage, PDRectangle rectangle, PDSignature signature) throws IOException {
        PDAcroForm acroForm = pdDocument.getDocumentCatalog().getAcroForm();
        List<PDField> acroFormFields = acroForm.getFields();
    
        PDSignatureField signatureField = new PDSignatureField(acroForm);
        signatureField.setSignature(signature);
        PDAnnotationWidget widget = signatureField.getWidgets().get(0);
        acroFormFields.add(signatureField);
    
        widget.setRectangle(rectangle);
        widget.setPage(pdPage);
    
        // from PDVisualSigBuilder.createHolderForm()
        PDStream stream = new PDStream(pdDocument);
        PDFormXObject form = new PDFormXObject(stream);
        PDResources res = new PDResources();
        form.setResources(res);
        form.setFormType(1);
        PDRectangle bbox = new PDRectangle(rectangle.getWidth(), rectangle.getHeight());
        float height = bbox.getHeight();
    
        form.setBBox(bbox);
        PDFont font = PDType1Font.HELVETICA_BOLD;
    
        // 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(pdDocument, appearanceStream))
        {
            // show background (just for debugging, to see the rect size + position)
            cs.setNonStrokingColor(Color.yellow);
            cs.addRect(-5000, -5000, 10000, 10000);
            cs.fill();
    
            float fontSize = 10;
            float leading = fontSize * 1.5f;
            cs.beginText();
            cs.setFont(font, fontSize);
            cs.setNonStrokingColor(Color.black);
            cs.newLineAtOffset(fontSize, height - leading);
            cs.setLeading(leading);
            cs.showText("Signature text");
            cs.newLine();
            cs.showText("some additional Information");
            cs.newLine();
            cs.showText("let's keep talking");
            cs.endText();
        }
    
        pdPage.getAnnotations().add(widget);
        
        COSDictionary pageTreeObject = pdPage.getCOSObject(); 
        while (pageTreeObject != null) {
            pageTreeObject.setNeedToBeUpdated(true);
            pageTreeObject = (COSDictionary) pageTreeObject.getDictionaryObject(COSName.PARENT);
        }
    }
    

    (CreateMultipleVisualizations helper method)

    (This method actually is based on the CreateVisibleSignature2.createVisualSignatureTemplate method from the pdfbox examples artifact but severely simplified and now used to create the actual signature fields, not merely a template to copy from.)

    Used like this

    try (   InputStream resource = PDF_SOURCE_STREAM;
            OutputStream result = PDF_TARGET_STREAM;
            PDDocument pdDocument = PDDocument.load(resource)   )
    {
        PDAcroForm acroForm = pdDocument.getDocumentCatalog().getAcroForm();
        if (acroForm == null) {
            pdDocument.getDocumentCatalog().setAcroForm(acroForm = new PDAcroForm(pdDocument));
        }
        acroForm.setSignaturesExist(true);
        acroForm.setAppendOnly(true);
        acroForm.getCOSObject().setDirect(true);
    
        PDRectangle rectangle = new PDRectangle(100, 600, 300, 100);
        PDSignature signature = new PDSignature();
        signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
        signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
        signature.setName("Example User");
        signature.setLocation("Los Angeles, CA");
        signature.setReason("Testing");
        signature.setSignDate(Calendar.getInstance());
        pdDocument.addSignature(signature, this);
    
        for (PDPage pdPage : pdDocument.getPages()) {
            addSignatureField(pdDocument, pdPage, rectangle, signature);
        }
    
        pdDocument.saveIncremental(result);
    }
    

    (CreateMultipleVisualizations test testCreateSignatureWithMultipleVisualizations)

    one retrieves a PDF with a signature visualization on each page of the result document (and an extra invisible one because I was a bit lazy) but only a single actual signature value (given that this implements SignatureInterface with the byte[] sign(InputStream) method).

    Beware, though:

    PS: If you want to get rid of that extra invisible signature, please have a look at this answer.