Digital text with text and background image我正在尝试使用Java中的PDFBox对pdf文件进行数字签名,并在页面上显示类似于在Acrobat中手动创建时创建的文本。如图所示(一个只有我正在寻找的快照和另一个有数字签名的细节),这个例子显示使用图像文件进行签名。怎么做?
答案 0 :(得分:3)
此代码将包含在即将发布的PDFbox 2.0.9版本的示例中。另见PDFBOX-3198中的讨论。它更灵活,既可以包含文本和图像,也可以包含两个中的一个,或者矢量图形,无论你想要什么。
/**
* This is a second example for visual signing a pdf. It doesn't use the "design pattern" influenced
* PDVisibleSignDesigner, and doesn't create its complex multilevel forms described in the Adobe
* document
* <a href="https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PPKAppearances.pdf">Digital
* Signature Appearances</a>, because this isn't required by the PDF specification. See the
* discussion in December 2017 in PDFBOX-3198.
*
* @author Vakhtang Koroghlishvili
* @author Tilman Hausherr
*/
public class CreateVisibleSignature2 extends CreateSignatureBase
{
private SignatureOptions signatureOptions;
private boolean lateExternalSigning = false;
private File imageFile;
/**
* Initialize the signature creator with a keystore (pkcs12) and pin that
* should be used for the signature.
*
* @param keystore is a pkcs12 keystore.
* @param pin is the pin for the keystore / private key
* @throws KeyStoreException if the keystore has not been initialized (loaded)
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
* @throws UnrecoverableKeyException if the given password is wrong
* @throws CertificateException if the certificate is not valid as signing time
* @throws IOException if no certificate could be found
*/
public CreateVisibleSignature2(KeyStore keystore, char[] pin)
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
{
super(keystore, pin);
}
public File getImageFile()
{
return imageFile;
}
public void setImageFile(File imageFile)
{
this.imageFile = imageFile;
}
public boolean isLateExternalSigning()
{
return lateExternalSigning;
}
/**
* Set late external signing. Enable this if you want to activate the demo code where the
* signature is kept and added in an extra step without using PDFBox methods. This is disabled
* by default.
*
* @param lateExternalSigning
*/
public void setLateExternalSigning(boolean lateExternalSigning)
{
this.lateExternalSigning = lateExternalSigning;
}
/**
* Sign pdf file and create new file that ends with "_signed.pdf".
*
* @param inputFile The source pdf document file.
* @param signedFile The file to be signed.
* @param humanRect rectangle from a human viewpoint (coordinates start at top left)
* @param tsaUrl optional TSA url
* @throws IOException
*/
public void signPDF(File inputFile, File signedFile, Rectangle2D humanRect, String tsaUrl) throws IOException
{
this.signPDF(inputFile, signedFile, humanRect, tsaUrl, null);
}
/**
* Sign pdf file and create new file that ends with "_signed.pdf".
*
* @param inputFile The source pdf document file.
* @param signedFile The file to be signed.
* @param humanRect rectangle from a human viewpoint (coordinates start at top left)
* @param tsaUrl optional TSA url
* @param signatureFieldName optional name of an existing (unsigned) signature field
* @throws IOException
*/
public void signPDF(File inputFile, File signedFile, Rectangle2D humanRect, String tsaUrl, String signatureFieldName) throws IOException
{
if (inputFile == null || !inputFile.exists())
{
throw new IOException("Document for signing does not exist");
}
setTsaUrl(tsaUrl);
// creating output document and prepare the IO streams.
FileOutputStream fos = new FileOutputStream(signedFile);
try (PDDocument doc = PDDocument.load(inputFile))
{
int accessPermissions = SigUtils.getMDPPermission(doc);
if (accessPermissions == 1)
{
throw new IllegalStateException("No changes to the document are permitted due to DocMDP transform parameters dictionary");
}
// Note that PDFBox has a bug that visual signing on certified files with permission 2
// doesn't work properly, see PDFBOX-3699. As long as this issue is open, you may want to
// be careful with such files.
PDSignature signature = null;
PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
PDRectangle rect = null;
// sign a PDF with an existing empty signature, as created by the CreateEmptySignatureForm example.
if (acroForm != null)
{
signature = findExistingSignature(acroForm, signatureFieldName);
if (signature != null)
{
rect = acroForm.getField(signatureFieldName).getWidgets().get(0).getRectangle();
}
}
if (signature == null)
{
// create signature dictionary
signature = new PDSignature();
}
if (rect == null)
{
rect = createSignatureRectangle(doc, humanRect);
}
// Optional: certify
// can be done only if version is at least 1.5 and if not already set
// doing this on a PDF/A-1b file fails validation by Adobe preflight (PDFBOX-3821)
// PDF/A-1b requires PDF version 1.4 max, so don't increase the version on such files.
if (doc.getVersion() >= 1.5f && accessPermissions == 0)
{
SigUtils.setMDPPermission(doc, signature, 2);
}
if (acroForm != null && acroForm.getNeedAppearances())
{
// PDFBOX-3738 NeedAppearances true results in visible signature becoming invisible
// with Adobe Reader
if (acroForm.getFields().isEmpty())
{
// we can safely delete it if there are no fields
acroForm.getCOSObject().removeItem(COSName.NEED_APPEARANCES);
// note that if you've set MDP permissions, the removal of this item
// may result in Adobe Reader claiming that the document has been changed.
// and/or that field content won't be displayed properly.
// ==> decide what you prefer and adjust your code accordingly.
}
else
{
System.out.println("/NeedAppearances is set, signature may be ignored by Adobe Reader");
}
}
// default filter
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
// subfilter for basic and PAdES Part 2 signatures
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName("Name");
signature.setLocation("Location");
signature.setReason("Reason");
// the signing date, needed for valid signature
signature.setSignDate(Calendar.getInstance());
// do not set SignatureInterface instance, if external signing used
SignatureInterface signatureInterface = isExternalSigning() ? null : this;
// register signature dictionary and sign interface
signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(createVisualSignatureTemplate(doc, 0, rect));
signatureOptions.setPage(0);
doc.addSignature(signature, signatureInterface, signatureOptions);
if (isExternalSigning())
{
System.out.println("Signing externally " + signedFile.getName());
ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);
// invoke external signature service
byte[] cmsSignature = sign(externalSigning.getContent());
// Explanation of late external signing (off by default):
// If you want to add the signature in a separate step, then set an empty byte array
// and call signature.getByteRange() and remember the offset signature.getByteRange()[1]+1.
// you can write the ascii hex signature at a later time even if you don't have this
// PDDocument object anymore, with classic java file random access methods.
// If you can't remember the offset value from ByteRange because your context has changed,
// then open the file with PDFBox, find the field with findExistingSignature() or
// PODDocument.getLastSignatureDictionary() and get the ByteRange from there.
// Close the file and then write the signature as explained earlier in this comment.
if (isLateExternalSigning())
{
// this saves the file with a 0 signature
externalSigning.setSignature(new byte[0]);
// remember the offset (add 1 because of "<")
int offset = signature.getByteRange()[1] + 1;
// now write the signature at the correct offset without any PDFBox methods
try (RandomAccessFile raf = new RandomAccessFile(signedFile, "rw"))
{
raf.seek(offset);
raf.write(Hex.getBytes(cmsSignature));
}
}
else
{
// set signature bytes received from the service and save the file
externalSigning.setSignature(cmsSignature);
}
}
else
{
// write incremental (only for signing purpose)
doc.saveIncremental(fos);
}
}
// Do not close signatureOptions before saving, because some COSStream objects within
// are transferred to the signed document.
// Do not allow signatureOptions get out of scope before saving, because then the COSDocument
// in signature options might by closed by gc, which would close COSStream objects prematurely.
// See https://issues.apache.org/jira/browse/PDFBOX-3743
IOUtils.closeQuietly(signatureOptions);
}
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();
// signing should be at the same position regardless of page rotation.
switch (page.getRotation())
{
case 90:
rect.setLowerLeftY(x);
rect.setUpperRightY(x + width);
rect.setLowerLeftX(y);
rect.setUpperRightX(y + height);
break;
case 180:
rect.setUpperRightX(pageRect.getWidth() - x);
rect.setLowerLeftX(pageRect.getWidth() - x - width);
rect.setLowerLeftY(y);
rect.setUpperRightY(y + height);
break;
case 270:
rect.setLowerLeftY(pageRect.getHeight() - x - width);
rect.setUpperRightY(pageRect.getHeight() - x);
rect.setLowerLeftX(pageRect.getWidth() - y - height);
rect.setUpperRightX(pageRect.getWidth() - y);
break;
case 0:
default:
rect.setLowerLeftX(x);
rect.setUpperRightX(x + width);
rect.setLowerLeftY(pageRect.getHeight() - y - height);
rect.setUpperRightY(pageRect.getHeight() - y);
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) 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());
float height = bbox.getHeight();
Matrix initialScale = null;
switch (srcDoc.getPage(pageNum).getRotation())
{
case 90:
form.setMatrix(AffineTransform.getQuadrantRotateInstance(1));
initialScale = Matrix.getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());
height = 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());
height = bbox.getWidth();
break;
case 0:
default:
break;
}
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(doc, appearanceStream))
{
// for 90° and 270° scale ratio of width / height
// not really sure about this
// why does scale have no effect when done in the form matrix???
if (initialScale != null)
{
cs.transform(initialScale);
}
// show background (just for debugging, to see the rect size + position)
cs.setNonStrokingColor(Color.yellow);
cs.addRect(-5000, -5000, 10000, 10000);
cs.fill();
// show background image
// save and restore graphics if the image is too large and needs to be scaled
cs.saveGraphicsState();
cs.transform(Matrix.getScaleInstance(0.25f, 0.25f));
PDImageXObject img = PDImageXObject.createFromFileByExtension(imageFile, doc);
cs.drawImage(img, 0, 0);
cs.restoreGraphicsState();
// show text
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 very wide line 1)");
cs.newLine();
cs.showText("(Signature very wide line 2)");
cs.newLine();
cs.showText("(Signature very wide line 3)");
cs.endText();
}
// no need to set annotations and /P entry
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
return new ByteArrayInputStream(baos.toByteArray());
}
}
// Find an existing signature (assumed to be empty). You will usually not need this.
private PDSignature findExistingSignature(PDAcroForm acroForm, String sigFieldName)
{
PDSignature signature = null;
PDSignatureField signatureField;
if (acroForm != null)
{
signatureField = (PDSignatureField) acroForm.getField(sigFieldName);
if (signatureField != null)
{
// retrieve signature dictionary
signature = signatureField.getSignature();
if (signature == null)
{
signature = new PDSignature();
// after solving PDFBOX-3524
// signatureField.setValue(signature)
// until then:
signatureField.getCOSObject().setItem(COSName.V, signature);
}
else
{
throw new IllegalStateException("The signature field " + sigFieldName + " is already signed.");
}
}
}
return signature;
}
/**
* Arguments are
* [0] key store
* [1] pin
* [2] document that will be signed
* [3] image of visible signature
*
* @param args
* @throws java.security.KeyStoreException
* @throws java.security.cert.CertificateException
* @throws java.io.IOException
* @throws java.security.NoSuchAlgorithmException
* @throws java.security.UnrecoverableKeyException
*/
public static void main(String[] args) throws KeyStoreException, CertificateException,
IOException, NoSuchAlgorithmException, UnrecoverableKeyException
{
// generate with
// keytool -storepass 123456 -storetype PKCS12 -keystore file.p12 -genkey -alias client -keyalg RSA
if (args.length < 4)
{
usage();
System.exit(1);
}
String tsaUrl = null;
// External signing is needed if you are using an external signing service, e.g. to sign
// several files at once.
boolean externalSig = false;
for (int i = 0; i < args.length; i++)
{
if (args[i].equals("-tsa"))
{
i++;
if (i >= args.length)
{
usage();
System.exit(1);
}
tsaUrl = args[i];
}
if (args[i].equals("-e"))
{
externalSig = true;
}
}
File ksFile = new File(args[0]);
KeyStore keystore = KeyStore.getInstance("PKCS12");
char[] pin = args[1].toCharArray();
keystore.load(new FileInputStream(ksFile), pin);
File documentFile = new File(args[2]);
CreateVisibleSignature2 signing = new CreateVisibleSignature2(keystore, pin.clone());
signing.setImageFile(new File(args[3]));
File signedDocumentFile;
String name = documentFile.getName();
String substring = name.substring(0, name.lastIndexOf('.'));
signedDocumentFile = new File(documentFile.getParent(), substring + "_signed.pdf");
signing.setExternalSigning(externalSig);
// Set the signature rectangle
// Although PDF coordinates start from the bottom, humans start from the top.
// So a human would want to position a signature (x,y) units from the
// top left of the displayed page, and the field has a horizontal width and a vertical height
// regardless of page rotation.
Rectangle2D humanRect = new Rectangle2D.Float(100, 200, 150, 50);
signing.signPDF(documentFile, signedDocumentFile, humanRect, tsaUrl, "Signature1");
}
/**
* This will print the usage for this program.
*/
private static void usage()
{
System.err.println("Usage: java " + CreateVisibleSignature2.class.getName()
+ " <pkcs12-keystore-file> <pin> <input-pdf> <sign-image>\n" + "" +
"options:\n" +
" -tsa <url> sign timestamp using the given TSA server\n"+
" -e sign using external signature creation scenario");
}
}