com.itextpdf.text.pdf.PdfSignatureAppearance Maven / Gradle / Ivy
Show all versions of itextpdf Show documentation
/*
*
* This file is part of the iText (R) project.
* Copyright (c) 1998-2016 iText Group NV
* Authors: Bruno Lowagie, Paulo Soares, et al.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation with the addition of the
* following permission added to Section 15 as permitted in Section 7(a):
* FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
* ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
* OF THIRD PARTY RIGHTS
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA, 02110-1301 USA, or download the license from the following URL:
* http://itextpdf.com/terms-of-use/
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License.
*
* In accordance with Section 7(b) of the GNU Affero General Public License,
* a covered work must retain the producer line in every PDF that is created
* or manipulated using iText.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the iText software without
* disclosing the source code of your own applications.
* These activities include: offering paid services to customers as an ASP,
* serving PDFs on the fly in a web application, shipping iText with a closed
* source product.
*
* For more information, please contact iText Software Corp. at this
* address: [email protected]
*/
package com.itextpdf.text.pdf;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.Image;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.Version;
import com.itextpdf.text.error_messages.MessageLocalization;
import com.itextpdf.text.io.RASInputStream;
import com.itextpdf.text.io.RandomAccessSource;
import com.itextpdf.text.io.RandomAccessSourceFactory;
import com.itextpdf.text.pdf.AcroFields.Item;
import com.itextpdf.text.pdf.interfaces.PdfVersion;
import com.itextpdf.text.pdf.security.CertificateInfo;
import com.itextpdf.text.pdf.security.CertificateInfo.X500Name;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
/**
* Class that takes care of the cryptographic options
* and appearances that form a signature.
*/
public class PdfSignatureAppearance {
/**
* Constructs a PdfSignatureAppearance object.
* @param writer the writer to which the signature will be written.
*/
PdfSignatureAppearance(PdfStamperImp writer) {
this.writer = writer;
signDate = new GregorianCalendar();
fieldName = getNewSigName();
signatureCreator = Version.getInstance().getVersion();
}
/*
* SIGNATURE
*/
// signature types
/** Approval signature */
public static final int NOT_CERTIFIED = 0;
/** Author signature, no changes allowed */
public static final int CERTIFIED_NO_CHANGES_ALLOWED = 1;
/** Author signature, form filling allowed */
public static final int CERTIFIED_FORM_FILLING = 2;
/** Author signature, form filling and annotations allowed */
public static final int CERTIFIED_FORM_FILLING_AND_ANNOTATIONS = 3;
/** The certification level */
private int certificationLevel = NOT_CERTIFIED;
/**
* Sets the document type to certified instead of simply signed.
* @param certificationLevel the values can be: NOT_CERTIFIED
, CERTIFIED_NO_CHANGES_ALLOWED
,
* CERTIFIED_FORM_FILLING
and CERTIFIED_FORM_FILLING_AND_ANNOTATIONS
*/
public void setCertificationLevel(int certificationLevel) {
this.certificationLevel = certificationLevel;
}
/**
* Gets the certified status of this document.
* @return the certified status
*/
public int getCertificationLevel() {
return this.certificationLevel;
}
// signature info
/** The caption for the reason for signing. */
private String reasonCaption = "Reason: ";
/** The caption for the location of signing. */
private String locationCaption = "Location: ";
/** The reason for signing. */
private String reason;
/** Holds value of property location. */
private String location;
/** Holds value of property signDate. */
private Calendar signDate;
/**
* Gets the signing reason.
* @return the signing reason
*/
public String getReason() {
return this.reason;
}
/**
* Sets the signing reason.
* @param reason the signing reason
*/
public void setReason(String reason) {
this.reason = reason;
}
/**
* Sets the caption for signing reason.
* @param reasonCaption the signing reason caption
*/
public void setReasonCaption(String reasonCaption) {
this.reasonCaption = reasonCaption;
}
/**
* Gets the signing location.
* @return the signing location
*/
public String getLocation() {
return this.location;
}
/**
* Sets the signing location.
* @param location the signing location
*/
public void setLocation(String location) {
this.location = location;
}
/**
* Sets the caption for the signing location.
* @param locationCaption the signing location caption
*/
public void setLocationCaption(String locationCaption) {
this.locationCaption = locationCaption;
}
/** Holds value of the application that creates the signature */
private String signatureCreator;
/**
* Gets the signature creator.
* @return the signature creator
*/
public String getSignatureCreator(){
return signatureCreator;
}
/**
* Sets the name of the application used to create the signature.
* @param signatureCreator the name of the signature creating application
*/
public void setSignatureCreator(String signatureCreator){
this.signatureCreator = signatureCreator;
}
/** The contact name of the signer. */
private String contact;
/**
* Gets the signing contact.
* @return the signing contact
*/
public String getContact() {
return this.contact;
}
/**
* Sets the signing contact.
* @param contact the signing contact
*/
public void setContact(String contact) {
this.contact = contact;
}
/**
* Gets the signature date.
* @return the signature date
*/
public java.util.Calendar getSignDate() {
return signDate;
}
/**
* Sets the signature date.
* @param signDate the signature date
*/
public void setSignDate(java.util.Calendar signDate) {
this.signDate = signDate;
}
// the PDF file
/** The file right before the signature is added (can be null). */
private RandomAccessFile raf;
/** The bytes of the file right before the signature is added (if raf is null) */
private byte[] bout;
/** Array containing the byte positions of the bytes that need to be hashed. */
private long[] range;
/**
* Gets the document bytes that are hashable when using external signatures. The general sequence is:
* preClose(), getRangeStream() and close().
*
* @return the document bytes that are hashable
*/
public InputStream getRangeStream() throws IOException {
RandomAccessSourceFactory fac = new RandomAccessSourceFactory();
return new RASInputStream(fac.createRanged(getUnderlyingSource(), range));
}
/**
* @return the underlying source
* @throws IOException
*/
private RandomAccessSource getUnderlyingSource() throws IOException {
//TODO: get rid of separate byte[] and RandomAccessFile objects and just store a RandomAccessSource
RandomAccessSourceFactory fac = new RandomAccessSourceFactory();
return raf == null ? fac.createSource(bout) : fac.createSource(raf);
}
/** The signing certificate */
private Certificate signCertificate;
// Developer extenstion
/**
* Adds the appropriate developer extension.
*/
public void addDeveloperExtension(final PdfDeveloperExtension de) {
writer.addDeveloperExtension(de);
}
// Crypto dictionary
/** The crypto dictionary */
private PdfDictionary cryptoDictionary;
/**
* Gets the user made signature dictionary. This is the dictionary at the /V key.
* @return the user made signature dictionary
*/
public com.itextpdf.text.pdf.PdfDictionary getCryptoDictionary() {
return cryptoDictionary;
}
/**
* Sets a user made signature dictionary. This is the dictionary at the /V key.
* @param cryptoDictionary a user made signature dictionary
*/
public void setCryptoDictionary(com.itextpdf.text.pdf.PdfDictionary cryptoDictionary) {
this.cryptoDictionary = cryptoDictionary;
}
/**
* Sets the certificate used to provide the text in the appearance.
* This certificate doesn't take part in the actual signing process.
* @param signCertificate the certificate
*/
public void setCertificate(Certificate signCertificate) {
this.signCertificate = signCertificate;
}
public Certificate getCertificate() {
return signCertificate;
}
// Signature event
/**
* An interface to retrieve the signature dictionary for modification.
*/
public interface SignatureEvent {
/**
* Allows modification of the signature dictionary.
* @param sig the signature dictionary
*/
public void getSignatureDictionary(PdfDictionary sig);
}
/**
* Holds value of property signatureEvent.
*/
private SignatureEvent signatureEvent;
/**
* Getter for property signatureEvent.
* @return Value of property signatureEvent.
*/
public SignatureEvent getSignatureEvent() {
return this.signatureEvent;
}
/**
* Sets the signature event to allow modification of the signature dictionary.
* @param signatureEvent the signature event
*/
public void setSignatureEvent(SignatureEvent signatureEvent) {
this.signatureEvent = signatureEvent;
}
/*
* SIGNATURE FIELD
*/
/** The name of the field */
private String fieldName;
/**
* Gets the field name.
* @return the field name
*/
public java.lang.String getFieldName() {
return fieldName;
}
/**
* Gets a new signature field name that
* doesn't clash with any existing name.
* @return a new signature field name
*/
public String getNewSigName() {
AcroFields af = writer.getAcroFields();
String name = "Signature";
int step = 0;
boolean found = false;
while (!found) {
++step;
String n1 = name + step;
if (af.getFieldItem(n1) != null)
continue;
n1 += ".";
found = true;
for (Object element : af.getFields().keySet()) {
String fn = (String)element;
if (fn.startsWith(n1)) {
found = false;
break;
}
}
}
name += step;
return name;
}
/**
* The page where the signature will appear.
*/
private int page = 1;
/**
* Gets the page number of the field.
* @return the page number of the field
*/
public int getPage() {
return page;
}
/**
* The coordinates of the rectangle for a visible signature,
* or a zero-width, zero-height rectangle for an invisible signature.
*/
private Rectangle rect;
/**
* Gets the rectangle representing the signature dimensions.
* @return the rectangle representing the signature dimensions. It may be null
* or have zero width or height for invisible signatures
*/
public Rectangle getRect() {
return rect;
}
/** rectangle that represent the position and dimension of the signature in the page. */
private Rectangle pageRect;
/**
* Gets the rectangle that represent the position and dimension of the signature in the page.
* @return the rectangle that represent the position and dimension of the signature in the page
*/
public Rectangle getPageRect() {
return pageRect;
}
/**
* Gets the visibility status of the signature.
* @return the visibility status of the signature
*/
public boolean isInvisible() {
return rect == null || rect.getWidth() == 0 || rect.getHeight() == 0;
}
/**
* Sets the signature to be visible. It creates a new visible signature field.
* @param pageRect the position and dimension of the field in the page
* @param page the page to place the field. The fist page is 1
* @param fieldName the field name or null
to generate automatically a new field name
*/
public void setVisibleSignature(Rectangle pageRect, int page, String fieldName) {
if (fieldName != null) {
if (fieldName.indexOf('.') >= 0)
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("field.names.cannot.contain.a.dot"));
AcroFields af = writer.getAcroFields();
AcroFields.Item item = af.getFieldItem(fieldName);
if (item != null)
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("the.field.1.already.exists", fieldName));
this.fieldName = fieldName;
}
if (page < 1 || page > writer.reader.getNumberOfPages())
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("invalid.page.number.1", page));
this.pageRect = new Rectangle(pageRect);
this.pageRect.normalize();
rect = new Rectangle(this.pageRect.getWidth(), this.pageRect.getHeight());
this.page = page;
}
/**
* Sets the signature to be visible. An empty signature field with the same name must already exist.
* @param fieldName the existing empty signature field name
*/
public void setVisibleSignature(String fieldName) {
AcroFields af = writer.getAcroFields();
AcroFields.Item item = af.getFieldItem(fieldName);
if (item == null)
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("the.field.1.does.not.exist", fieldName));
PdfDictionary merged = item.getMerged(0);
if (!PdfName.SIG.equals(PdfReader.getPdfObject(merged.get(PdfName.FT))))
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("the.field.1.is.not.a.signature.field", fieldName));
this.fieldName = fieldName;
PdfArray r = merged.getAsArray(PdfName.RECT);
float llx = r.getAsNumber(0).floatValue();
float lly = r.getAsNumber(1).floatValue();
float urx = r.getAsNumber(2).floatValue();
float ury = r.getAsNumber(3).floatValue();
pageRect = new Rectangle(llx, lly, urx, ury);
pageRect.normalize();
page = item.getPage(0).intValue();
int rotation = writer.reader.getPageRotation(page);
Rectangle pageSize = writer.reader.getPageSizeWithRotation(page);
switch (rotation) {
case 90:
pageRect = new Rectangle(
pageRect.getBottom(),
pageSize.getTop() - pageRect.getLeft(),
pageRect.getTop(),
pageSize.getTop() - pageRect.getRight());
break;
case 180:
pageRect = new Rectangle(
pageSize.getRight() - pageRect.getLeft(),
pageSize.getTop() - pageRect.getBottom(),
pageSize.getRight() - pageRect.getRight(),
pageSize.getTop() - pageRect.getTop());
break;
case 270:
pageRect = new Rectangle(
pageSize.getRight() - pageRect.getBottom(),
pageRect.getLeft(),
pageSize.getRight() - pageRect.getTop(),
pageRect.getRight());
break;
}
if (rotation != 0)
pageRect.normalize();
rect = new Rectangle(this.pageRect.getWidth(), this.pageRect.getHeight());
}
/*
* SIGNATURE APPEARANCE
*/
/**
* Signature rendering modes
* @since 5.0.1
*/
public enum RenderingMode {
/**
* The rendering mode is just the description.
*/
DESCRIPTION,
/**
* The rendering mode is the name of the signer and the description.
*/
NAME_AND_DESCRIPTION,
/**
* The rendering mode is an image and the description.
*/
GRAPHIC_AND_DESCRIPTION,
/**
* The rendering mode is just an image.
*/
GRAPHIC
}
/** The rendering mode chosen for visible signatures */
private RenderingMode renderingMode = RenderingMode.DESCRIPTION;
/**
* Gets the rendering mode for this signature.
* @return the rendering mode for this signature
* @since 5.0.1
*/
public RenderingMode getRenderingMode() {
return renderingMode;
}
/**
* Sets the rendering mode for this signature.
* @param renderingMode the rendering mode
* @since 5.0.1
*/
public void setRenderingMode(RenderingMode renderingMode) {
this.renderingMode = renderingMode;
}
/** The image that needs to be used for a visible signature */
private Image signatureGraphic = null;
/**
* Gets the Image object to render.
* @return the image
*/
public Image getSignatureGraphic() {
return signatureGraphic;
}
/**
* Sets the Image object to render when Render is set to RenderingMode.GRAPHIC
* or RenderingMode.GRAPHIC_AND_DESCRIPTION
.
* @param signatureGraphic image rendered. If null
the mode is defaulted
* to RenderingMode.DESCRIPTION
*/
public void setSignatureGraphic(Image signatureGraphic) {
this.signatureGraphic = signatureGraphic;
}
/** Appearance compliant with the recommendations introduced in Acrobat 6? */
private boolean acro6Layers = true;
/**
* Gets the Acrobat 6.0 layer mode.
* @return the Acrobat 6.0 layer mode
*/
public boolean isAcro6Layers() {
return this.acro6Layers;
}
/**
* Acrobat 6.0 and higher recommends that only layer n0 and n2 be present.
* Use this method with value false
if you want to ignore this recommendation.
* @param acro6Layers if true
only the layers n0 and n2 will be present
* @deprecated Adobe no longer supports Adobe Acrobat / Reader versions older than 9
*/
public void setAcro6Layers(boolean acro6Layers) {
this.acro6Layers = acro6Layers;
}
/** Layers for a visible signature. */
private PdfTemplate app[] = new PdfTemplate[5];
/**
* Gets a template layer to create a signature appearance. The layers can go from 0 to 4,
* but only layer 0 and 2 will be used if acro6Layers is true.
*
* Consult PPKAppearances.pdf
* for further details.
* @param layer the layer
* @return a template
*/
public PdfTemplate getLayer(int layer) {
if (layer < 0 || layer >= app.length)
return null;
PdfTemplate t = app[layer];
if (t == null) {
t = app[layer] = new PdfTemplate(writer);
t.setBoundingBox(rect);
writer.addDirectTemplateSimple(t, new PdfName("n" + layer));
}
return t;
}
/** Indicates if we need to reuse the existing appearance as layer 0. */
private boolean reuseAppearance = false;
/**
* Indicates that the existing appearances needs to be reused as layer 0.
*/
public void setReuseAppearance(boolean reuseAppearance) {
this.reuseAppearance = reuseAppearance;
}
// layer 1
/** An appearance that can be used for layer 1 (if acro6Layers is false). */
public static final String questionMark =
"% DSUnknown\n" +
"q\n" +
"1 G\n" +
"1 g\n" +
"0.1 0 0 0.1 9 0 cm\n" +
"0 J 0 j 4 M []0 d\n" +
"1 i \n" +
"0 g\n" +
"313 292 m\n" +
"313 404 325 453 432 529 c\n" +
"478 561 504 597 504 645 c\n" +
"504 736 440 760 391 760 c\n" +
"286 760 271 681 265 626 c\n" +
"265 625 l\n" +
"100 625 l\n" +
"100 828 253 898 381 898 c\n" +
"451 898 679 878 679 650 c\n" +
"679 555 628 499 538 435 c\n" +
"488 399 467 376 467 292 c\n" +
"313 292 l\n" +
"h\n" +
"308 214 170 -164 re\n" +
"f\n" +
"0.44 G\n" +
"1.2 w\n" +
"1 1 0.4 rg\n" +
"287 318 m\n" +
"287 430 299 479 406 555 c\n" +
"451 587 478 623 478 671 c\n" +
"478 762 414 786 365 786 c\n" +
"260 786 245 707 239 652 c\n" +
"239 651 l\n" +
"74 651 l\n" +
"74 854 227 924 355 924 c\n" +
"425 924 653 904 653 676 c\n" +
"653 581 602 525 512 461 c\n" +
"462 425 441 402 441 318 c\n" +
"287 318 l\n" +
"h\n" +
"282 240 170 -164 re\n" +
"B\n" +
"Q\n";
// layer 2
/** A background image for the text in layer 2. */
private Image image;
/**
* Gets the background image for the layer 2.
* @return the background image for the layer 2
*/
public Image getImage() {
return this.image;
}
/**
* Sets the background image for the layer 2.
* @param image the background image for the layer 2
*/
public void setImage(Image image) {
this.image = image;
}
/** the scaling to be applied to the background image.t */
private float imageScale;
/**
* Gets the scaling to be applied to the background image.
* @return the scaling to be applied to the background image
*/
public float getImageScale() {
return this.imageScale;
}
/**
* Sets the scaling to be applied to the background image. If it's zero the image
* will fully fill the rectangle. If it's less than zero the image will fill the rectangle but
* will keep the proportions. If it's greater than zero that scaling will be applied.
* In any of the cases the image will always be centered. It's zero by default.
* @param imageScale the scaling to be applied to the background image
*/
public void setImageScale(float imageScale) {
this.imageScale = imageScale;
}
/** The text that goes in Layer 2 of the signature appearance. */
private String layer2Text;
/**
* Sets the signature text identifying the signer.
* @param text the signature text identifying the signer. If null
or not set
* a standard description will be used
*/
public void setLayer2Text(String text) {
layer2Text = text;
}
/**
* Gets the signature text identifying the signer if set by setLayer2Text().
* @return the signature text identifying the signer
*/
public String getLayer2Text() {
return layer2Text;
}
/** Font for the text in Layer 2. */
private Font layer2Font;
/**
* Gets the n2 and n4 layer font.
* @return the n2 and n4 layer font
*/
public Font getLayer2Font() {
return this.layer2Font;
}
/**
* Sets the n2 and n4 layer font. If the font size is zero, auto-fit will be used.
* @param layer2Font the n2 and n4 font
*/
public void setLayer2Font(Font layer2Font) {
this.layer2Font = layer2Font;
}
/** Run direction for the text in layers 2 and 4. */
private int runDirection = PdfWriter.RUN_DIRECTION_NO_BIDI;
/** Sets the run direction in the n2 and n4 layer.
* @param runDirection the run direction
*/
public void setRunDirection(int runDirection) {
if (runDirection < PdfWriter.RUN_DIRECTION_DEFAULT || runDirection > PdfWriter.RUN_DIRECTION_RTL)
throw new RuntimeException(MessageLocalization.getComposedMessage("invalid.run.direction.1", runDirection));
this.runDirection = runDirection;
}
/** Gets the run direction.
* @return the run direction
*/
public int getRunDirection() {
return runDirection;
}
// layer 4
/** The text that goes in Layer 4 of the appearance. */
private String layer4Text;
/**
* Sets the text identifying the signature status. Will be ignored if acro6Layers is true.
* @param text the text identifying the signature status. If null
or not set
* the description "Signature Not Verified" will be used
*/
public void setLayer4Text(String text) {
layer4Text = text;
}
/**
* Gets the text identifying the signature status if set by setLayer4Text().
* @return the text identifying the signature status
*/
public String getLayer4Text() {
return layer4Text;
}
// all layers
/** Template containing all layers drawn on top of each other. */
private PdfTemplate frm;
/**
* Gets the template that aggregates all appearance layers. This corresponds to the /FRM resource.
*
* Consult PPKAppearances.pdf
* for further details.
* @return the template that aggregates all appearance layers
*/
public PdfTemplate getTopLayer() {
if (frm == null) {
frm = new PdfTemplate(writer);
frm.setBoundingBox(rect);
writer.addDirectTemplateSimple(frm, new PdfName("FRM"));
}
return frm;
}
// creating the appearance
/** extra space at the top. */
private static final float TOP_SECTION = 0.3f;
/** margin for the content inside the signature rectangle. */
private static final float MARGIN = 2;
/**
* Gets the main appearance layer.
*
* Consult PPKAppearances.pdf
* for further details.
* @return the main appearance layer
* @throws DocumentException on error
*/
public PdfTemplate getAppearance() throws DocumentException {
if (isInvisible()) {
PdfTemplate t = new PdfTemplate(writer);
t.setBoundingBox(new Rectangle(0, 0));
writer.addDirectTemplateSimple(t, null);
return t;
}
if (app[0] == null && !reuseAppearance) {
createBlankN0();
}
if (app[1] == null && !acro6Layers) {
PdfTemplate t = app[1] = new PdfTemplate(writer);
t.setBoundingBox(new Rectangle(100, 100));
writer.addDirectTemplateSimple(t, new PdfName("n1"));
t.setLiteral(questionMark);
}
if (app[2] == null) {
String text;
if (layer2Text == null) {
StringBuilder buf = new StringBuilder();
buf.append("Digitally signed by ");
String name = null;
X500Name x500name = CertificateInfo.getSubjectFields((X509Certificate)signCertificate);
if (x500name != null) {
name = x500name.getField("CN");
if (name == null)
name = x500name.getField("E");
}
if (name == null)
name = "";
buf.append(name).append('\n');
SimpleDateFormat sd = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z");
buf.append("Date: ").append(sd.format(signDate.getTime()));
if (reason != null)
buf.append('\n').append(reasonCaption).append(reason);
if (location != null)
buf.append('\n').append(locationCaption).append(location);
text = buf.toString();
}
else
text = layer2Text;
PdfTemplate t = app[2] = new PdfTemplate(writer);
t.setBoundingBox(rect);
writer.addDirectTemplateSimple(t, new PdfName("n2"));
if (image != null) {
if (imageScale == 0) {
t.addImage(image, rect.getWidth(), 0, 0, rect.getHeight(), 0, 0);
}
else {
float usableScale = imageScale;
if (imageScale < 0)
usableScale = Math.min(rect.getWidth() / image.getWidth(), rect.getHeight() / image.getHeight());
float w = image.getWidth() * usableScale;
float h = image.getHeight() * usableScale;
float x = (rect.getWidth() - w) / 2;
float y = (rect.getHeight() - h) / 2;
t.addImage(image, w, 0, 0, h, x, y);
}
}
Font font;
if (layer2Font == null)
font = new Font();
else
font = new Font(layer2Font);
float size = font.getSize();
Rectangle dataRect = null;
Rectangle signatureRect = null;
if (renderingMode == RenderingMode.NAME_AND_DESCRIPTION ||
renderingMode == RenderingMode.GRAPHIC_AND_DESCRIPTION && this.signatureGraphic != null) {
// origin is the bottom-left
signatureRect = new Rectangle(
MARGIN,
MARGIN,
rect.getWidth() / 2 - MARGIN,
rect.getHeight() - MARGIN);
dataRect = new Rectangle(
rect.getWidth() / 2 + MARGIN / 2,
MARGIN,
rect.getWidth() - MARGIN / 2,
rect.getHeight() - MARGIN);
if (rect.getHeight() > rect.getWidth()) {
signatureRect = new Rectangle(
MARGIN,
rect.getHeight() / 2,
rect.getWidth() - MARGIN,
rect.getHeight());
dataRect = new Rectangle(
MARGIN,
MARGIN,
rect.getWidth() - MARGIN,
rect.getHeight() / 2 - MARGIN);
}
}
else if (renderingMode == RenderingMode.GRAPHIC) {
if (signatureGraphic == null) {
throw new IllegalStateException(MessageLocalization.getComposedMessage("a.signature.image.should.be.present.when.rendering.mode.is.graphic.only"));
}
signatureRect = new Rectangle(
MARGIN,
MARGIN,
rect.getWidth() - MARGIN, // take all space available
rect.getHeight() - MARGIN);
}
else {
dataRect = new Rectangle(
MARGIN,
MARGIN,
rect.getWidth() - MARGIN,
rect.getHeight() * (1 - TOP_SECTION) - MARGIN);
}
switch (renderingMode) {
case NAME_AND_DESCRIPTION:
String signedBy = CertificateInfo.getSubjectFields((X509Certificate)signCertificate).getField("CN");
if (signedBy == null)
signedBy = CertificateInfo.getSubjectFields((X509Certificate)signCertificate).getField("E");
if (signedBy == null)
signedBy = "";
Rectangle sr2 = new Rectangle(signatureRect.getWidth() - MARGIN, signatureRect.getHeight() - MARGIN );
float signedSize = ColumnText.fitText(font, signedBy, sr2, -1, runDirection);
ColumnText ct2 = new ColumnText(t);
ct2.setRunDirection(runDirection);
ct2.setSimpleColumn(new Phrase(signedBy, font), signatureRect.getLeft(), signatureRect.getBottom(), signatureRect.getRight(), signatureRect.getTop(), signedSize, Element.ALIGN_LEFT);
ct2.go();
break;
case GRAPHIC_AND_DESCRIPTION:
if (signatureGraphic == null) {
throw new IllegalStateException(MessageLocalization.getComposedMessage("a.signature.image.should.be.present.when.rendering.mode.is.graphic.and.description"));
}
ct2 = new ColumnText(t);
ct2.setRunDirection(runDirection);
ct2.setSimpleColumn(signatureRect.getLeft(), signatureRect.getBottom(), signatureRect.getRight(), signatureRect.getTop(), 0, Element.ALIGN_RIGHT);
Image im = Image.getInstance(signatureGraphic);
im.scaleToFit(signatureRect.getWidth(), signatureRect.getHeight());
Paragraph p = new Paragraph();
// must calculate the point to draw from to make image appear in middle of column
float x = 0;
// experimentation found this magic number to counteract Adobe's signature graphic, which
// offsets the y co-ordinate by 15 units
float y = -im.getScaledHeight() + 15;
x = x + (signatureRect.getWidth() - im.getScaledWidth()) / 2;
y = y - (signatureRect.getHeight() - im.getScaledHeight()) / 2;
p.add(new Chunk(im, x + (signatureRect.getWidth() - im.getScaledWidth()) / 2, y, false));
ct2.addElement(p);
ct2.go();
break;
case GRAPHIC:
ct2 = new ColumnText(t);
ct2.setRunDirection(runDirection);
ct2.setSimpleColumn(signatureRect.getLeft(), signatureRect.getBottom(), signatureRect.getRight(), signatureRect.getTop(), 0, Element.ALIGN_RIGHT);
im = Image.getInstance(signatureGraphic);
im.scaleToFit(signatureRect.getWidth(), signatureRect.getHeight());
p = new Paragraph(signatureRect.getHeight());
// must calculate the point to draw from to make image appear in middle of column
x = (signatureRect.getWidth() - im.getScaledWidth()) / 2;
y = (signatureRect.getHeight() - im.getScaledHeight()) / 2;
p.add(new Chunk(im, x, y, false));
ct2.addElement(p);
ct2.go();
break;
default:
}
if(renderingMode != RenderingMode.GRAPHIC) {
if (size <= 0) {
Rectangle sr = new Rectangle(dataRect.getWidth(), dataRect.getHeight());
size = ColumnText.fitText(font, text, sr, 12, runDirection);
}
ColumnText ct = new ColumnText(t);
ct.setRunDirection(runDirection);
ct.setSimpleColumn(new Phrase(text, font), dataRect.getLeft(), dataRect.getBottom(), dataRect.getRight(), dataRect.getTop(), size, Element.ALIGN_LEFT);
ct.go();
}
}
if (app[3] == null && !acro6Layers) {
PdfTemplate t = app[3] = new PdfTemplate(writer);
t.setBoundingBox(new Rectangle(100, 100));
writer.addDirectTemplateSimple(t, new PdfName("n3"));
t.setLiteral("% DSBlank\n");
}
if (app[4] == null && !acro6Layers) {
PdfTemplate t = app[4] = new PdfTemplate(writer);
t.setBoundingBox(new Rectangle(0, rect.getHeight() * (1 - TOP_SECTION), rect.getRight(), rect.getTop()));
writer.addDirectTemplateSimple(t, new PdfName("n4"));
Font font;
if (layer2Font == null)
font = new Font();
else
font = new Font(layer2Font);
//float size = font.getSize();
String text = "Signature Not Verified";
if (layer4Text != null)
text = layer4Text;
Rectangle sr = new Rectangle(rect.getWidth() - 2 * MARGIN, rect.getHeight() * TOP_SECTION - 2 * MARGIN);
float size = ColumnText.fitText(font, text, sr, 15, runDirection);
ColumnText ct = new ColumnText(t);
ct.setRunDirection(runDirection);
ct.setSimpleColumn(new Phrase(text, font), MARGIN, 0, rect.getWidth() - MARGIN, rect.getHeight() - MARGIN, size, Element.ALIGN_LEFT);
ct.go();
}
int rotation = writer.reader.getPageRotation(page);
Rectangle rotated = new Rectangle(rect);
int n = rotation;
while (n > 0) {
rotated = rotated.rotate();
n -= 90;
}
if (frm == null) {
frm = new PdfTemplate(writer);
frm.setBoundingBox(rotated);
writer.addDirectTemplateSimple(frm, new PdfName("FRM"));
float scale = Math.min(rect.getWidth(), rect.getHeight()) * 0.9f;
float x = (rect.getWidth() - scale) / 2;
float y = (rect.getHeight() - scale) / 2;
scale /= 100;
if (rotation == 90)
frm.concatCTM(0, 1, -1, 0, rect.getHeight(), 0);
else if (rotation == 180)
frm.concatCTM(-1, 0, 0, -1, rect.getWidth(), rect.getHeight());
else if (rotation == 270)
frm.concatCTM(0, -1, 1, 0, 0, rect.getWidth());
if (reuseAppearance) {
AcroFields af = writer.getAcroFields();
PdfIndirectReference ref = af.getNormalAppearance(getFieldName());
if (ref != null) {
frm.addTemplateReference(ref, new PdfName("n0"), 1, 0, 0, 1, 0, 0);
}
else {
reuseAppearance = false;
if (app[0] == null) {
createBlankN0();
}
}
}
if (!reuseAppearance) {
frm.addTemplate(app[0], 0, 0);
}
if (!acro6Layers)
frm.addTemplate(app[1], scale, 0, 0, scale, x, y);
frm.addTemplate(app[2], 0, 0);
if (!acro6Layers) {
frm.addTemplate(app[3], scale, 0, 0, scale, x, y);
frm.addTemplate(app[4], 0, 0);
}
}
PdfTemplate napp = new PdfTemplate(writer);
napp.setBoundingBox(rotated);
writer.addDirectTemplateSimple(napp, null);
napp.addTemplate(frm, 0, 0);
return napp;
}
private void createBlankN0() {
PdfTemplate t = app[0] = new PdfTemplate(writer);
t.setBoundingBox(new Rectangle(100, 100));
writer.addDirectTemplateSimple(t, new PdfName("n0"));
t.setLiteral("% DSBlank\n");
}
/*
* Creating the signed file.
*/
/** The PdfStamper that creates the signed PDF. */
private PdfStamper stamper;
/**
* Gets the PdfStamper
associated with this instance.
* @return the PdfStamper
associated with this instance
*/
public PdfStamper getStamper() {
return stamper;
}
/**
* Sets the PdfStamper
* @param stamper PdfStamper
*/
void setStamper(PdfStamper stamper) {
this.stamper = stamper;
}
/** The PdfStamperImp object corresponding with the stamper. */
private PdfStamperImp writer;
/** A byte buffer containing the bytes of the Stamper. */
private ByteBuffer sigout;
/**
* Getter for the byte buffer.
*/
ByteBuffer getSigout() {
return sigout;
}
/**
* Setter for the byte buffer.
*/
void setSigout(ByteBuffer sigout) {
this.sigout = sigout;
}
/** OutputStream for the bytes of the stamper. */
private OutputStream originalout;
/**
* Getter for the OutputStream.
*/
OutputStream getOriginalout() {
return originalout;
}
/**
* Setter for the OutputStream.
*/
void setOriginalout(OutputStream originalout) {
this.originalout = originalout;
}
/** Temporary file in case you don't want to sign in memory. */
private File tempFile;
/**
* Gets the temporary file.
* @return the temporary file or null
is the document is created in memory
*/
public File getTempFile() {
return tempFile;
}
/**
* Setter for the temporary file.
* @param tempFile
*/
void setTempFile(File tempFile) {
this.tempFile = tempFile;
}
/** Name and content of keys that can only be added in the close() method. */
private HashMap exclusionLocations;
/** Length of the output. */
private int boutLen;
/** Indicates if the stamper has already been pre-closed. */
private boolean preClosed = false;
/** Signature field lock dictionary */
private PdfSigLockDictionary fieldLock;
/**
* Getter for the field lock dictionary.
* @return Field lock dictionary.
*/
public PdfSigLockDictionary getFieldLockDict() {
return fieldLock;
}
/**
* Setter for the field lock dictionary.
* Be aware: if a signature is created on an existing signature field,
* then its /Lock dictionary takes the precedence (if it exists).
*
* @param fieldLock Field lock dictionary.
*/
public void setFieldLockDict(PdfSigLockDictionary fieldLock) {
this.fieldLock = fieldLock;
}
/**
* Checks if the document is in the process of closing.
* @return true
if the document is in the process of closing,
* false
otherwise
*/
public boolean isPreClosed() {
return preClosed;
}
/**
* This is the first method to be called when using external signatures. The general sequence is:
* preClose(), getDocumentBytes() and close().
*
* If calling preClose() dont't call PdfStamper.close().
*
* exclusionSizes
must contain at least
* the PdfName.CONTENTS
key with the size that it will take in the
* document. Note that due to the hex string coding this size should be
* byte_size*2+2.
* @param exclusionSizes a HashMap
with names and sizes to be excluded in the signature
* calculation. The key is a PdfName
and the value an
* Integer
. At least the PdfName.CONTENTS
must be present
* @throws IOException on error
* @throws DocumentException on error
*/
public void preClose(HashMap exclusionSizes) throws IOException, DocumentException {
if (preClosed)
throw new DocumentException(MessageLocalization.getComposedMessage("document.already.pre.closed"));
stamper.mergeVerification();
preClosed = true;
AcroFields af = writer.getAcroFields();
String name = getFieldName();
boolean fieldExists = af.doesSignatureFieldExist(name);
PdfIndirectReference refSig = writer.getPdfIndirectReference();
writer.setSigFlags(3);
PdfDictionary fieldLock = null;
if (fieldExists) {
PdfDictionary widget = af.getFieldItem(name).getWidget(0);
writer.markUsed(widget);
fieldLock = widget.getAsDict(PdfName.LOCK);
if (fieldLock == null && this.fieldLock != null) {
widget.put(PdfName.LOCK, writer.addToBody(this.fieldLock).getIndirectReference());
fieldLock = this.fieldLock;
}
widget.put(PdfName.P, writer.getPageReference(getPage()));
widget.put(PdfName.V, refSig);
PdfObject obj = PdfReader.getPdfObjectRelease(widget.get(PdfName.F));
int flags = 0;
if (obj != null && obj.isNumber())
flags = ((PdfNumber)obj).intValue();
flags |= PdfAnnotation.FLAGS_LOCKED;
widget.put(PdfName.F, new PdfNumber(flags));
PdfDictionary ap = new PdfDictionary();
ap.put(PdfName.N, getAppearance().getIndirectReference());
widget.put(PdfName.AP, ap);
}
else {
PdfFormField sigField = PdfFormField.createSignature(writer);
sigField.setFieldName(name);
sigField.put(PdfName.V, refSig);
sigField.setFlags(PdfAnnotation.FLAGS_PRINT | PdfAnnotation.FLAGS_LOCKED);
if (this.fieldLock != null) {
sigField.put(PdfName.LOCK, writer.addToBody(this.fieldLock).getIndirectReference());
fieldLock = this.fieldLock;
}
int pagen = getPage();
if (!isInvisible())
sigField.setWidget(getPageRect(), null);
else
sigField.setWidget(new Rectangle(0, 0), null);
sigField.setAppearance(PdfAnnotation.APPEARANCE_NORMAL, getAppearance());
sigField.setPage(pagen);
writer.addAnnotation(sigField, pagen);
}
exclusionLocations = new HashMap();
if (cryptoDictionary == null) {
throw new DocumentException("No crypto dictionary defined.");
}
else {
PdfLiteral lit = new PdfLiteral(80);
exclusionLocations.put(PdfName.BYTERANGE, lit);
cryptoDictionary.put(PdfName.BYTERANGE, lit);
for (Map.Entry entry: exclusionSizes.entrySet()) {
PdfName key = entry.getKey();
Integer v = entry.getValue();
lit = new PdfLiteral(v.intValue());
exclusionLocations.put(key, lit);
cryptoDictionary.put(key, lit);
}
if (certificationLevel > 0)
addDocMDP(cryptoDictionary);
if (fieldLock != null)
addFieldMDP(cryptoDictionary, fieldLock);
if (signatureEvent != null)
signatureEvent.getSignatureDictionary(cryptoDictionary);
writer.addToBody(cryptoDictionary, refSig, false);
}
if (certificationLevel > 0) {
// add DocMDP entry to root
PdfDictionary docmdp = new PdfDictionary();
docmdp.put(new PdfName("DocMDP"), refSig);
writer.reader.getCatalog().put(new PdfName("Perms"), docmdp);
}
writer.close(stamper.getMoreInfo());
range = new long[exclusionLocations.size() * 2];
long byteRangePosition = exclusionLocations.get(PdfName.BYTERANGE).getPosition();
exclusionLocations.remove(PdfName.BYTERANGE);
int idx = 1;
for (PdfLiteral lit: exclusionLocations.values()) {
long n = lit.getPosition();
range[idx++] = n;
range[idx++] = lit.getPosLength() + n;
}
Arrays.sort(range, 1, range.length - 1);
for (int k = 3; k < range.length - 2; k += 2)
range[k] -= range[k - 1];
if (tempFile == null) {
bout = sigout.getBuffer();
boutLen = sigout.size();
range[range.length - 1] = boutLen - range[range.length - 2];
ByteBuffer bf = new ByteBuffer();
bf.append('[');
for (int k = 0; k < range.length; ++k)
bf.append(range[k]).append(' ');
bf.append(']');
System.arraycopy(bf.getBuffer(), 0, bout, (int)byteRangePosition, bf.size());
}
else {
try {
raf = new RandomAccessFile(tempFile, "rw");
long len = raf.length();
range[range.length - 1] = len - range[range.length - 2];
ByteBuffer bf = new ByteBuffer();
bf.append('[');
for (int k = 0; k < range.length; ++k)
bf.append(range[k]).append(' ');
bf.append(']');
raf.seek(byteRangePosition);
raf.write(bf.getBuffer(), 0, bf.size());
}
catch (IOException e) {
try{raf.close();}catch(Exception ee){}
try{tempFile.delete();}catch(Exception ee){}
throw e;
}
}
}
/**
* Adds keys to the signature dictionary that define
* the certification level and the permissions.
* This method is only used for Certifying signatures.
* @param crypto the signature dictionary
*/
private void addDocMDP(PdfDictionary crypto) {
PdfDictionary reference = new PdfDictionary();
PdfDictionary transformParams = new PdfDictionary();
transformParams.put(PdfName.P, new PdfNumber(certificationLevel));
transformParams.put(PdfName.V, new PdfName("1.2"));
transformParams.put(PdfName.TYPE, PdfName.TRANSFORMPARAMS);
reference.put(PdfName.TRANSFORMMETHOD, PdfName.DOCMDP);
reference.put(PdfName.TYPE, PdfName.SIGREF);
reference.put(PdfName.TRANSFORMPARAMS, transformParams);
if (writer.getPdfVersion().getVersion() < PdfWriter.VERSION_1_6) {
reference.put(new PdfName("DigestValue"), new PdfString("aa"));
PdfArray loc = new PdfArray();
loc.add(new PdfNumber(0));
loc.add(new PdfNumber(0));
reference.put(new PdfName("DigestLocation"), loc);
reference.put(new PdfName("DigestMethod"), new PdfName("MD5"));
}
reference.put(PdfName.DATA, writer.reader.getTrailer().get(PdfName.ROOT));
PdfArray types = new PdfArray();
types.add(reference);
crypto.put(PdfName.REFERENCE, types);
}
/**
* Adds keys to the signature dictionary that define
* the field permissions.
* This method is only used for signatures that lock fields.
* @param crypto the signature dictionary
*/
private void addFieldMDP(PdfDictionary crypto, PdfDictionary fieldLock) {
PdfDictionary reference = new PdfDictionary();
PdfDictionary transformParams = new PdfDictionary();
transformParams.putAll(fieldLock);
transformParams.put(PdfName.TYPE, PdfName.TRANSFORMPARAMS);
transformParams.put(PdfName.V, new PdfName("1.2"));
reference.put(PdfName.TRANSFORMMETHOD, PdfName.FIELDMDP);
reference.put(PdfName.TYPE, PdfName.SIGREF);
reference.put(PdfName.TRANSFORMPARAMS, transformParams);
reference.put(new PdfName("DigestValue"), new PdfString("aa"));
PdfArray loc = new PdfArray();
loc.add(new PdfNumber(0));
loc.add(new PdfNumber(0));
reference.put(new PdfName("DigestLocation"), loc);
reference.put(new PdfName("DigestMethod"), new PdfName("MD5"));
reference.put(PdfName.DATA, writer.reader.getTrailer().get(PdfName.ROOT));
PdfArray types = crypto.getAsArray(PdfName.REFERENCE);
if (types == null)
types = new PdfArray();
types.add(reference);
crypto.put(PdfName.REFERENCE, types);
}
/**
* This is the last method to be called when using external signatures. The general sequence is:
* preClose(), getDocumentBytes() and close().
*
* update
is a PdfDictionary
that must have exactly the
* same keys as the ones provided in {@link #preClose(HashMap)}.
* @param update a PdfDictionary
with the key/value that will fill the holes defined
* in {@link #preClose(HashMap)}
* @throws DocumentException on error
* @throws IOException on error
*/
public void close(PdfDictionary update) throws IOException, DocumentException {
try {
if (!preClosed)
throw new DocumentException(MessageLocalization.getComposedMessage("preclose.must.be.called.first"));
ByteBuffer bf = new ByteBuffer();
for (PdfName key: update.getKeys()) {
PdfObject obj = update.get(key);
PdfLiteral lit = exclusionLocations.get(key);
if (lit == null)
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("the.key.1.didn.t.reserve.space.in.preclose", key.toString()));
bf.reset();
obj.toPdf(null, bf);
if (bf.size() > lit.getPosLength())
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("the.key.1.is.too.big.is.2.reserved.3", key.toString(), String.valueOf(bf.size()), String.valueOf(lit.getPosLength())));
if (tempFile == null)
System.arraycopy(bf.getBuffer(), 0, bout, (int)lit.getPosition(), bf.size());
else {
raf.seek(lit.getPosition());
raf.write(bf.getBuffer(), 0, bf.size());
}
}
if (update.size() != exclusionLocations.size())
throw new IllegalArgumentException(MessageLocalization.getComposedMessage("the.update.dictionary.has.less.keys.than.required"));
if (tempFile == null) {
originalout.write(bout, 0, boutLen);
}
else {
if (originalout != null) {
raf.seek(0);
long length = raf.length();
byte buf[] = new byte[8192];
while (length > 0) {
int r = raf.read(buf, 0, (int)Math.min((long)buf.length, length));
if (r < 0)
throw new EOFException(MessageLocalization.getComposedMessage("unexpected.eof"));
originalout.write(buf, 0, r);
length -= r;
}
}
}
}
finally {
writer.reader.close();
if (tempFile != null) {
try{raf.close();}catch(Exception ee){}
if (originalout != null)
try{tempFile.delete();}catch(Exception ee){}
}
if (originalout != null)
try{originalout.close();}catch(Exception e){}
}
}
}