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.