Код: Выделить всё
/*
* Heavily influenced by:
* https://github.com/apache/pdfbox/blob/3.0/examples/src/main/java/org/apache/pdfbox/examples/signature/CreateVisibleSignature.java
*/
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.Calendar;
@Slf4j
@Component
public class CreateVisibleSignature extends CreateSignatureBase {
private static final float SIGNATURE_HEIGHT = 25;
@Value("${signature.name}")
private String name;
@Value("${signature.location}")
private String location;
@Value("${signature.reason}")
private String reason;
@Value("${signature.keystore}")
private String keystorePath;
@Value("${signature.pin}")
private String pin;
private PDVisibleSignDesigner visibleSignDesigner;
private final PDVisibleSigProperties visibleSignatureProperties = new PDVisibleSigProperties();
@PostConstruct
public void init() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException {
KeyStore keystore = loadKeyStore();
super.initialize(keystore, pin.toCharArray());
Security.addProvider(new BouncyCastleProvider());
setVisibleSignatureProperties(name, location, reason, 0, true);
}
public void signPDF(PDDocument document, PDImageXObject pdImage, ByteArrayOutputStream fos, PDSignatureField pdSignatureField, int pageNum, boolean isFirstSignature, boolean isLastSignature) throws IOException {
// 1. Check if the document can be modified.
checkDocumentPermissions(document);
// 2. Find the existing signature
PDSignature signature = findExistingSignature(document, pdSignatureField);
// // 3. Adjust signature field size
// adjustSignatureFieldSize(pdSignatureField, pdImage);
// 4. Set Visible Sign Designer and known bug check
setVisibleSignDesigner(document, pdImage, pageNum);
// 5. Set Document Permissions
setDocumentPermissions(document, signature, isFirstSignature, isLastSignature);
// 6. Set Signature Properties
setSignatureProperties(signature);
// 7. Build Signature
buildSignature(pageNum);
// 8. Add Signing Date
signature.setSignDate(Calendar.getInstance());
// 9. Sign the document
SignatureOptions signatureOptions = new SignatureOptions();
// TODO make sure getVisibleSignature is not null!
signatureOptions.setVisualSignature(visibleSignatureProperties.getVisibleSignature());
signatureOptions.setPage(visibleSignatureProperties.getPage() - 1);
document.addSignature(signature, this, signatureOptions);
// 10. Save incremental (Must be done to add multiple signatures)
document.saveIncremental(fos);
IOUtils.closeQuietly(signatureOptions);
}
/*
* MDP (Modification Detection and Prevention) Permission Levels:
*
* 0: No DocMDP transform parameters dictionary exists (document is not certified)
*
* 1: No changes to the document are permitted
* - Most restrictive
* - Forbids even adding more signatures
*
* 2: Allows filling in forms and digitally signing
* - Default if /P entry is missing or invalid
* - Forbids other modifications (e.g., adding/deleting pages, editing content)
*
* 3: Allows filling in forms, digitally signing, and adding comments/annotations
* - Least restrictive
* - Still forbids modifying the document's core content
*
* Note: Attempting to make changes beyond what's allowed by the permission level will invalidate the certification signature.
* At least in theory though, in practice, when I set the permissions, then add another signature, the first in invalidated.
*/
private void setDocumentPermissions(PDDocument document, PDSignature signature, boolean isFirstSignature, boolean isLastSignature) throws IOException {
// The first signatures must be the certification signature, the following signatures must be approval signatures
if (document.getVersion() >= 1.5f) {
if (isFirstSignature) {
if (SigUtils.getMDPPermission(document) == 0) {
// See notes above. Maybe we can get away with this until we figure it out
SigUtils.setMDPPermission(document, signature,2);
} else {
throw new IllegalStateException("Document is already certified");
}
}
} else {
if (!isDocumentCertified(document)) {
log.warn("Adding approval signature to an uncertified document");
}
}
}
private void setSignatureProperties(PDSignature signature){
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(visibleSignatureProperties.getSignerName());
signature.setLocation(visibleSignatureProperties.getSignerLocation());
signature.setReason(visibleSignatureProperties.getSignatureReason());
}
private void buildSignature(int pageNum) throws IOException {
visibleSignatureProperties.page(pageNum);
visibleSignatureProperties.setPdVisibleSignature(visibleSignDesigner);
visibleSignatureProperties.buildSignature();
}
private void checkDocumentPermissions(PDDocument document) {
// If permissions 1, then no changes are permitted
int accessPermissions = SigUtils.getMDPPermission(document);
if (accessPermissions == 1) {
throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
}
}
private boolean isDocumentCertified(PDDocument document) {
COSDictionary permsDict = document.getDocumentCatalog().getCOSObject()
.getCOSDictionary(COSName.PERMS);
return permsDict != null && permsDict.containsKey(COSName.DOCMDP);
}
public void setVisibleSignDesigner(PDDocument document, PDImageXObject pdImage, int page) throws IOException {
visibleSignDesigner = new PDVisibleSignDesigner(document, pdImage.getImage(), page);
// Calculate aspect ratio
float originalWidth = pdImage.getWidth();
float originalHeight = pdImage.getHeight();
float aspectRatio = originalWidth / originalHeight;
// Set desired height and calculate corresponding width to maintain aspect ratio
float desiredWidth = SIGNATURE_HEIGHT * aspectRatio;
// Set the size and position of the signature image
visibleSignDesigner.xAxis(0).yAxis(0).width(desiredWidth).height(SIGNATURE_HEIGHT).adjustForRotation();
}
public void setVisibleSignatureProperties(String name, String location, String reason, int preferredSize, boolean visualSignEnabled) {
visibleSignatureProperties.signerName(name).signerLocation(location).signatureReason(reason).
preferredSize(preferredSize).visualSignEnabled(visualSignEnabled).
setPdVisibleSignature(visibleSignDesigner);
}
private PDSignature findExistingSignature(PDDocument doc, PDSignatureField signatureField) {
PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
if (acroForm == null) {
throw new IllegalStateException("Document does not contain an AcroForm");
}
PDSignature signature = signatureField.getSignature();
if (signature == null) {
signature = new PDSignature();
signatureField.getCOSObject().setItem(COSName.V, signature);
}
return signature;
}
private KeyStore loadKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream is = CreateVisibleSignature.class.getResourceAsStream(keystorePath)) {
ks.load(is, pin.toCharArray());
}
return ks;
}
}
Код: Выделить всё
private byte[] applyAllSignatures(byte[] pdfBytes, List allSignatures) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int i = 0; i < allSignatures.size(); i++) {
try (PDDocument pdf = Loader.loadPDF(pdfBytes)) {
SignatureInfo signatureInfo = allSignatures.get(i);
boolean isFirstSignature = (i == 0);
boolean isLastSignature = (i == allSignatures.size() - 1);
PDSignatureField signatureField = findSignatureField(pdf, signatureInfo.fieldName());
if (signatureField != null) {
createVisibleSignature.signPDF(
pdf,
buildSignatureImage(signatureInfo.sigFile(), pdf),
baos,
signatureField,
getPageNumber(pdf, signatureField),
isFirstSignature,
isLastSignature
);
pdfBytes = baos.toByteArray();
baos.reset();
}
}
}
return pdfBytes;
}
Вторая подпись выглядит нормально, но "Поля формы с изменениями свойств" удивляют, как и моя сертификат уже недействителен. Конечно, любой вклад будет принят с благодарностью.
Что интересно, если я вернусь к версии PDFBox 2.0.32 без каких-либо других изменений, обе подписи будут выглядеть именно так, как я ожидал. ; однако они невидимы.

Подробнее здесь: https://stackoverflow.com/questions/788 ... fbox-3-0-2