diff --git a/core/core-awt/src/main/java/module-info.java b/core/core-awt/src/main/java/module-info.java index 13102a16d..2c53f4d77 100644 --- a/core/core-awt/src/main/java/module-info.java +++ b/core/core-awt/src/main/java/module-info.java @@ -30,10 +30,14 @@ exports org.icepdf.core.pobjects; exports org.icepdf.core.pobjects.acroform; exports org.icepdf.core.pobjects.acroform.signature; + exports org.icepdf.core.pobjects.acroform.signature.appearance; exports org.icepdf.core.pobjects.acroform.signature.certificates; exports org.icepdf.core.pobjects.acroform.signature.exceptions; + exports org.icepdf.core.pobjects.acroform.signature.handlers; + exports org.icepdf.core.pobjects.acroform.signature.utils; exports org.icepdf.core.pobjects.actions; exports org.icepdf.core.pobjects.annotations; + exports org.icepdf.core.pobjects.annotations.utils; exports org.icepdf.core.pobjects.fonts; exports org.icepdf.core.pobjects.graphics; exports org.icepdf.core.pobjects.graphics.commands; diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Catalog.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Catalog.java index 52aa98e7b..199d77fb9 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Catalog.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Catalog.java @@ -35,7 +35,7 @@ * class for convenience, but can also be accessed via the {@link PTrailer} class. * Useful information about the document can be extracted from the Catalog * Dictionary, such as PDF version information and Viewer Preferences. All - * Catalog dictionary properties can be accesed via the getEntries method. + * Catalog dictionary properties can be accessed via the getEntries method. * See section 3.6.1 of the PDF Reference version 1.6 for more information on * the properties available in the Catalog Object.

* @@ -415,6 +415,23 @@ public InteractiveForm getInteractiveForm() { return interactiveForm; } + /** + * Gets the interactive form object that contains the form widgets for the given PDF. This method should be + * called before adding new widgets. + * + * @return The interactive form object if it exists, if null a new dictionary is inserted into the document. + */ + public InteractiveForm getOrCreateInteractiveForm() { + if (interactiveForm == null) { + interactiveForm = new InteractiveForm(library, new DictionaryEntries()); + StateManager stateManager = library.getStateManager(); + this.entries.put(ACRO_FORM_KEY, interactiveForm); + stateManager.addChange(new PObject(this, this.getPObjectReference())); + return interactiveForm; + } + return interactiveForm; + } + /** * Returns a summary of the Catalog dictionary values. * diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Document.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Document.java index cb4c87d44..14db05a31 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Document.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Document.java @@ -649,7 +649,7 @@ public long saveToOutputStream(OutputStream out, WriteMode writeMode) throws IOE * There are two possible entries, SCREEN and PRINT each with configurable * rendering hints settings. * @param pageBoundary Constant specifying the page boundary to use when - * painting the page content. Typically use Page.BOUNDARY_CROPBOX. + * painting the page content. Typically, use Page.BOUNDARY_CROPBOX. * @param userRotation Rotation factor, in degrees, to be applied to the rendered page. * Arbitrary rotations are not currently supported for this method, * so only the following values are valid: 0.0f, 90.0f, 180.0f, 270.0f. diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Form.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Form.java index a1fec805f..69b287291 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Form.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Form.java @@ -18,6 +18,7 @@ import org.icepdf.core.pobjects.graphics.ExtGState; import org.icepdf.core.pobjects.graphics.GraphicsState; import org.icepdf.core.pobjects.graphics.Shapes; +import org.icepdf.core.pobjects.graphics.images.ImageStream; import org.icepdf.core.util.Library; import org.icepdf.core.util.parser.content.ContentParser; import org.icepdf.core.util.updater.callbacks.ContentStreamCallback; @@ -28,6 +29,10 @@ import java.util.logging.Level; import java.util.logging.Logger; +import static org.icepdf.core.pobjects.Resources.FONT_KEY; +import static org.icepdf.core.pobjects.Resources.XOBJECT_KEY; +import static org.icepdf.core.pobjects.annotations.FreeTextAnnotation.EMBEDDED_FONT_NAME; + /** * Form XObject class. Not currently part of the public api. *
@@ -140,7 +145,7 @@ private static AffineTransform getAffineTransform(List v) { /** * As of the PDF 1.2 specification, a resource entry is not required for - * a XObject and thus it needs to point to the parent resource to enable + * a XObject, thus it needs to point to the parent resource to enable * to correctly load the content stream. * * @param parentResource parent objects resourse when available. @@ -212,6 +217,64 @@ public void setResources(Resources resources) { entries.put(RESOURCES_KEY, resources.getEntries()); } + /** + * Add a Font resource to this Form's resource dictionary. This is intended for new object only, can't guarantee + * this method will work as expected on an existing object. + * + * @param fontDictionary font dictionary to add as a resource + */ + public void addFontResource(DictionaryEntries fontDictionary) { + StateManager stateManager = library.getStateManager(); + org.icepdf.core.pobjects.fonts.Font newFont; + Resources formResources = getResources(); + DictionaryEntries fontsDictionary = formResources.getFonts(); + if (fontsDictionary == null) { + fontsDictionary = new DictionaryEntries(); + formResources.entries.put(FONT_KEY, fontsDictionary); + } + if (formResources.getFont(EMBEDDED_FONT_NAME) == null) { + // set up a new font resource + newFont = new org.icepdf.core.pobjects.fonts.zfont.SimpleFont(library, fontDictionary); + newFont.setPObjectReference(stateManager.getNewReferenceNumber()); + // create font entry + fontsDictionary.put(EMBEDDED_FONT_NAME, newFont.getPObjectReference()); + // sync font resources with form object. + entries.put(RESOURCES_KEY, formResources.entries); + } else { + // reuse previously defined 'common' annotation font + newFont = formResources.getFont(EMBEDDED_FONT_NAME); + Reference reference = newFont.getPObjectReference(); + newFont = new org.icepdf.core.pobjects.fonts.zfont.SimpleFont(library, fontDictionary); + newFont.setPObjectReference(reference); + } + + // update hard reference to state manager and weak library reference. + stateManager.addChange(new PObject(newFont, newFont.getPObjectReference()), isNew); + library.addObject(newFont, newFont.getPObjectReference()); + // make sure the form changes get picked up as well + stateManager.addChange(new PObject(this, getPObjectReference()), isNew); + } + + /** + * Add an ImageStream to this Form's resource dictionary. This is intended for new object only, can't guarantee + * this method will work as expected on an existing object. + * + * @param imageName named image + * @param imageStream corresponding ImageStream data + */ + public void addImageResource(Name imageName, ImageStream imageStream) { + Resources formResources = getResources(); + DictionaryEntries xObjectsDictionary = formResources.getXObjects(); + if (xObjectsDictionary == null) { + xObjectsDictionary = new DictionaryEntries(); + formResources.entries.put(XOBJECT_KEY, xObjectsDictionary); + } + // sync form resources with form object. + entries.put(RESOURCES_KEY, formResources.entries); + xObjectsDictionary.put(imageName, imageStream.getPObjectReference()); + StateManager stateManager = library.getStateManager(); + stateManager.addChange(new PObject(this, getPObjectReference()), isNew); + } /** * Gets the shapes that where parsed from the content stream. diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/HexStringObject.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/HexStringObject.java index 2d3dfba64..743f8c2f6 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/HexStringObject.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/HexStringObject.java @@ -67,6 +67,21 @@ public static HexStringObject createHexString(String literalstring) { return new HexStringObject(hexString.toString()); } + public static String encodeHexString(byte[] byteArray) { + StringBuffer hexStringBuffer = new StringBuffer(); + for (int i = 0; i < byteArray.length; i++) { + hexStringBuffer.append(byteToHex(byteArray[i])); + } + return hexStringBuffer.toString(); + } + + private static String byteToHex(byte num) { + char[] hexDigits = new char[2]; + hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16); + hexDigits[1] = Character.forDigit((num & 0xF), 16); + return new String(hexDigits); + } + /** * Encodes the given contents string into a 4 byte hex string. This allows us to easily account for * mixed encoding of 2-byte and 4 byte string content. @@ -281,7 +296,8 @@ private static StringBuilder normalizeHex(StringBuilder hex, int step) { if (step == 2) { // pre append 0's to uneven length, be careful as the 0020 isn't the same as 2000 if (length % 2 != 0) { - hex = new StringBuilder("0").append(hex); + // this was done for variable byte font encoding, this seems risky to preappend, pulling + hex = hex.append("0");//new StringBuilder("0").append(hex); } } if (step == 4) { diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Resources.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Resources.java index aa06d6c41..52f01852c 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/Resources.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/Resources.java @@ -51,8 +51,8 @@ private static synchronized int getUniqueId() { private static final Logger logger = Logger.getLogger(Resources.class.toString()); - final DictionaryEntries fonts; - final DictionaryEntries xobjects; + DictionaryEntries fonts; + DictionaryEntries xobjects; final DictionaryEntries colorspaces; final DictionaryEntries patterns; final DictionaryEntries shading; @@ -71,6 +71,9 @@ public Resources(Library l, DictionaryEntries h) { } public DictionaryEntries getFonts() { + if (fonts == null) { + fonts = library.getDictionary(entries, FONT_KEY); + } return fonts; } @@ -213,6 +216,9 @@ public Object getXObject(Name s) { } public DictionaryEntries getXObjects() { + if (xobjects == null) { + xobjects = library.getDictionary(entries, XOBJECT_KEY); + } return xobjects; } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/StateManager.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/StateManager.java index e5b66634b..dfd07cbae 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/StateManager.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/StateManager.java @@ -43,6 +43,7 @@ public class StateManager { private final CrossReferenceRoot crossReferenceRoot; private final AtomicInteger nextReferenceNumber; + private final AtomicInteger nextImageNumber; // snapshot of currently saved changes private Map savedChangesSnapshot = new HashMap<>(); @@ -61,6 +62,8 @@ public StateManager(CrossReferenceRoot crossReferenceRoot) { // thus the next available number. nextReferenceNumber = new AtomicInteger(); nextReferenceNumber.set(this.crossReferenceRoot.getNextAvailableReferenceNumber()); + // named image reference count + nextImageNumber = new AtomicInteger((int) (Math.random() * 1000)); } /** @@ -75,6 +78,17 @@ public Reference getNewReferenceNumber() { return new Reference(nextReferenceNumber.getAndIncrement(), 0); } + /** + * Gets an image number to be used when generating image references. The initial value is randomly + * generated and incremented for each addition image added to the document. There should be very little + * chance that signature would have the same name. + * + * @return unique image number for building images names + */ + public int getNextImageNumber() { + return nextImageNumber.getAndIncrement(); + } + /** * Add a new PObject containing changed data to the cache. * @@ -206,6 +220,12 @@ public CrossReferenceRoot getCrossReferenceRoot() { return crossReferenceRoot; } + /** + * Checks to see if any redaction annotations are present in the changes. This is used to determine if + * the document has redactions that need to be burned in. + * + * @return true if redactions are present, false otherwise. + */ public boolean hasRedactions() { if (changes.isEmpty()) return false; Collection changesValues = changes.values(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/DocMDPTransferParam.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/DocMDPTransferParam.java index a67bee0b9..c1dcf0c75 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/DocMDPTransferParam.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/DocMDPTransferParam.java @@ -90,6 +90,10 @@ public int getPermissions() { * @return always returns 1.2 as a name. */ public Name getVersion() { + return DocMDPTransferParam.getDocMDPVersion(); + } + + public static Name getDocMDPVersion() { return new Name("1.2"); } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/InteractiveForm.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/InteractiveForm.java index 3de572b25..205117a10 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/InteractiveForm.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/InteractiveForm.java @@ -16,11 +16,13 @@ package org.icepdf.core.pobjects.acroform; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.annotations.AbstractWidgetAnnotation; import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; import org.icepdf.core.util.Library; import org.icepdf.core.util.Utils; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Logger; @@ -206,6 +208,36 @@ public synchronized void init() { } } + public void addField(Object field) { + if (!(field instanceof AbstractWidgetAnnotation)) { + throw new IllegalStateException("Field must be an AbstractWidgetAnnotation"); + } + if (fields == null) { + fields = new ArrayList<>(); + fields.add(field); + entries.put(FIELDS_KEY, + new ArrayList<>(Arrays.asList(((AbstractWidgetAnnotation) field).getPObjectReference()))); + } else { + fields.add(field); + List fieldReferences = (List) library.getObject(entries, FIELDS_KEY); + fieldReferences.add(((AbstractWidgetAnnotation) field).getPObjectReference()); + } + // mark the catalog as changed, this object is always contained in the catalog as a dictionary, it + // should never be an indirect reference. + Catalog catalog = library.getCatalog(); + StateManager stateManager = library.getStateManager(); + stateManager.addChange(new PObject(catalog, catalog.getPObjectReference())); + } + + public void removeField(AbstractWidgetAnnotation field) { + if (fields != null) { + fields.remove(field); + List fieldReferences = (List) library.getObject(entries, FIELDS_KEY); + fieldReferences.remove((field).getPObjectReference()); + } + } + + /** * Gets the fields associated with this form. * @@ -216,7 +248,8 @@ public ArrayList getFields() { } /** - * Gets the signature fields associated with this form. A new array that references the forms signature annotations. + * Gets the signature fields associated with this form. A new array that references the forms signature + * annotations. * If no fields are found an empty list is returned. * * @return a list of form signature objects. @@ -351,7 +384,8 @@ public String getDefaultVariableTextDAField() { /** * Ges the default variable text quadding rule. * - * @return integer represented by VariableTextFieldDictionary.QUADDING_LEFT_JUSTIFIED, VariableTextFieldDictionary.QUADDING_CENTERED or + * @return integer represented by VariableTextFieldDictionary.QUADDING_LEFT_JUSTIFIED, + * VariableTextFieldDictionary.QUADDING_CENTERED or * VariableTextFieldDictionary.QUADDING_RIGHT_JUSTIFIED. */ public int getDefaultVariableTextQField() { diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureDictionary.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureDictionary.java index da00300e2..715fe2d33 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureDictionary.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureDictionary.java @@ -15,16 +15,36 @@ */ package org.icepdf.core.pobjects.acroform; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.operator.OperatorCreationException; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.acroform.signature.DocumentSigner; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureType; +import org.icepdf.core.pobjects.acroform.signature.handlers.SignerHandler; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; import org.icepdf.core.util.Library; import org.icepdf.core.util.Utils; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; +import static org.icepdf.core.pobjects.Permissions.DOC_MDP_KEY; +import static org.icepdf.core.pobjects.acroform.DocMDPTransferParam.PERMISSION_KEY; +import static org.icepdf.core.pobjects.acroform.DocMDPTransferParam.PERMISSION_VALUE_NO_CHANGES; +import static org.icepdf.core.pobjects.acroform.FieldDictionaryFactory.TYPE_SIGNATURE; +import static org.icepdf.core.pobjects.acroform.SignatureReferenceDictionary.*; +import static org.icepdf.core.pobjects.acroform.signature.DigitalSignatureFactory.DSS_SUB_FILTER_PKCS7_DETACHED; + /** - * A digital signature (PDF 1.3) may be used to authenticate the identity of a user and the document’s contents. It stores - * information about the signer and the state of the document when it was signed. The signature may be purely mathematical, + * A digital signature (PDF 1.3) may be used to authenticate the identity of a user and the document’s contents. It + * stores + * information about the signer and the state of the document when it was signed. The signature may be purely + * mathematical, * such as a public/private-key encrypted document digest, or it may be a biometric form of identification, such as a * handwritten signature, fingerprint, or retinal scan. The specific form of authentication used shall be implemented by * a special software module called a signature handler. @@ -45,7 +65,8 @@ public class SignatureDictionary extends Dictionary { public static final Name FILTER_KEY = new Name("Filter"); /** - * (Optional) A name that describes the encoding of the signature value and key information in the signature dictionary. + * (Optional) A name that describes the encoding of the signature value and key information in the signature + * dictionary. * A conforming reader may use any handler that supports this format to validate the signature. *
* (PDF 1.6) The following values for public-key cryptographic signatures shall be used: adbe.x509.rsa_sha1, @@ -152,7 +173,8 @@ public class SignatureDictionary extends Dictionary { * state of the computer environment used for signing, such as the name of the handler used to create the signature, * software build date, version, and operating system. *
- * The PDF Signature Build Dictionary Specification, provides implementation guidelines for the use of this dictionary. + * The PDF Signature Build Dictionary Specification, provides implementation guidelines for the use of this + * dictionary. */ public static final Name PROP_BUILD_KEY = new Name("Prop_Build"); @@ -169,17 +191,75 @@ public class SignatureDictionary extends Dictionary { public static final Name PROP_AUTH_TIME_KEY = new Name("Prop_AuthTime"); /** - * (Optional) Information provided by the signer to enable a recipient to contact the signer to verify the signature. + * (Optional) Information provided by the signer to enable a recipient to contact the signer to verify the + * signature. *
* EXAMPLE 3
* A phone number. */ public static final Name CONTACT_INFO_KEY = new Name("ContactInfo"); + private SignerHandler signerHandler; + public SignatureDictionary(Library library, DictionaryEntries entries) { super(library, entries); } + public static SignatureDictionary getInstance(SignatureWidgetAnnotation signatureWidgetAnnotation, + SignatureType signatureType) { + Library library = signatureWidgetAnnotation.getLibrary(); + DictionaryEntries signatureDictionaryEntries = new DictionaryEntries(); + + // reference dictionary + signatureDictionaryEntries.put(REFERENCE_KEY, List.of(buildReferenceDictionary(library, signatureType))); + + signatureDictionaryEntries.put(TYPE_KEY, TYPE_SIGNATURE); + signatureDictionaryEntries.put(FILTER_KEY, new Name("Adobe.PPKLite")); + signatureDictionaryEntries.put(SUB_FILTER_KEY, DSS_SUB_FILTER_PKCS7_DETACHED); + + // add placeholders for the signature, these values are updated when this object is written to disk + // and contents when the signature hash is calculated. + signatureDictionaryEntries.put(BYTE_RANGE_KEY, List.of(0, 0, 0, 0)); + signatureDictionaryEntries.put(CONTENTS_KEY, new HexStringObject(DocumentSigner.generateContentsPlaceholder())); + + // flag updater that signatureDictionary needs to be updated. + SignatureDictionary signatureDictionary = new SignatureDictionary(library, signatureDictionaryEntries); + StateManager stateManager = library.getStateManager(); + signatureDictionary.setPObjectReference(stateManager.getNewReferenceNumber()); + stateManager.addChange(new PObject(signatureDictionary, signatureDictionary.getPObjectReference())); + + // attach the dictionary to the annotation + signatureWidgetAnnotation.setSignatureDictionary(signatureDictionary); + + return signatureDictionary; + } + + private static SignatureReferenceDictionary buildReferenceDictionary(Library library, SignatureType signatureType) { + DictionaryEntries referenceEntries = new DictionaryEntries(); + referenceEntries.put(TYPE_KEY, SIG_REF_TYPE_VALUE); + referenceEntries.put(DIGEST_METHOD_KEY, new Name("SHA1")); + referenceEntries.put(TRANSFORM_METHOD_KEY, DOC_MDP_KEY); + + DictionaryEntries transformParams = new DictionaryEntries(); + transformParams.put(PERMISSION_KEY, PERMISSION_VALUE_NO_CHANGES); + transformParams.put(V_KEY, DocMDPTransferParam.getDocMDPVersion()); + if (signatureType.equals(SignatureType.CERTIFIER)) { + referenceEntries.put(TRANSFORM_PARAMS_KEY, new DocMDPTransferParam(library, transformParams)); + } + + return new SignatureReferenceDictionary(library, referenceEntries); + } + + public void setSignerHandler(SignerHandler signerHandler) { + this.signerHandler = signerHandler; + } + + public byte[] getSignedData(byte[] data) throws IOException, CMSException, UnrecoverableKeyException, + CertificateException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException { + // move to signature + return signerHandler.signData(data); + } + public Name getFilter() { return library.getName(entries, FILTER_KEY); } @@ -252,17 +332,8 @@ public void setByteRangeKey(ArrayList range) { entries.put(BYTE_RANGE_KEY, range); } - public ArrayList getReferences() { - List tmp = library.getArray(entries, REFERENCE_KEY); - if (tmp != null && tmp.size() > 0) { - ArrayList references = new ArrayList<>(tmp.size()); - for (DictionaryEntries reference : tmp) { - references.add(new SignatureReferenceDictionary(library, reference)); - } - return references; - } else { - return null; - } + public List getReferences() { + return library.getArray(entries, REFERENCE_KEY); } public ArrayList getChanges() { @@ -283,10 +354,18 @@ public String getName() { } } + public void setName(String name) { + entries.put(NAME_KEY, new LiteralStringObject(name)); + } + public String getDate() { return library.getString(entries, M_KEY); } + public void setDate(String date) { + entries.put(M_KEY, new LiteralStringObject(date)); + } + public String getLocation() { Object tmp = library.getObject(entries, LOCATION_KEY); if (tmp instanceof StringObject) { @@ -296,6 +375,10 @@ public String getLocation() { } } + public void setLocation(String location) { + entries.put(LOCATION_KEY, new LiteralStringObject(location)); + } + public String getReason() { Object tmp = library.getObject(entries, REASON_KEY); if (tmp instanceof StringObject) { @@ -305,6 +388,10 @@ public String getReason() { } } + public void setReason(String reason) { + entries.put(REASON_KEY, new LiteralStringObject(reason)); + } + public String getContactInfo() { Object tmp = library.getObject(entries, CONTACT_INFO_KEY); if (tmp instanceof StringObject) { @@ -314,6 +401,10 @@ public String getContactInfo() { } } + public void setContactInfo(String contactInfo) { + entries.put(CONTACT_INFO_KEY, new LiteralStringObject(contactInfo)); + } + public int getHandlerVersion() { return library.getInt(entries, R_KEY); } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureHandler.java index 0e1a081c5..7cb99b225 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureHandler.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/SignatureHandler.java @@ -36,32 +36,6 @@ public class SignatureHandler { private static final Logger logger = Logger.getLogger(SignatureHandler.class.toString()); - static { - // Load security handler from system property if possible - String defaultSecurityProvider = - "org.bouncycastle.jce.provider.BouncyCastleProvider"; - - // check system property security provider - String customSecurityProvider = - Defs.sysProperty("org.icepdf.core.security.jceProvider"); - - // if no custom security provider load default security provider - if (customSecurityProvider != null) { - defaultSecurityProvider = customSecurityProvider; - } - try { - // try and create a new provider - Object provider = Class.forName(defaultSecurityProvider).getDeclaredConstructor().newInstance(); - Security.insertProviderAt((Provider) provider, 2); - } catch (ClassNotFoundException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider not found"); - } catch (InstantiationException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider could not be instantiated"); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider could not be created"); - } - } - public SignatureHandler() { } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/AbstractPkcsValidator.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/AbstractPkcsValidator.java index b37454ca1..b7bd6ff16 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/AbstractPkcsValidator.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/AbstractPkcsValidator.java @@ -91,7 +91,7 @@ public abstract class AbstractPkcsValidator implements SignatureValidator { private boolean isCertificateDateValid = true; private boolean isRevocation; private boolean isSelfSigned; - // todo impelement singer time check. + // todo implement singer time check. private boolean isSignerTimeValid; private boolean isEmbeddedTimeStamp; // last time validate call was made. @@ -101,7 +101,9 @@ public abstract class AbstractPkcsValidator implements SignatureValidator { public AbstractPkcsValidator(SignatureFieldDictionary signatureFieldDictionary) throws SignatureIntegrityException { this.signatureFieldDictionary = signatureFieldDictionary; - init(); + if (signatureFieldDictionary != null) { + init(); + } } /** @@ -336,7 +338,6 @@ private ASN1Sequence parseCertificateData(byte[] cmsData, ASN1Sequence signedDat SubjectKeyIdentifier ::= OCTET STRING */ ASN1Sequence issuerAndSerialNumber = (ASN1Sequence) signerInfo.getObjectAt(1); - signerCertificate = null; if (signerVersion == 1) { // parse out the issue and SerialNumber. X500Principal issuer; @@ -347,7 +348,6 @@ private ASN1Sequence parseCertificateData(byte[] cmsData, ASN1Sequence signedDat throw new SignatureIntegrityException("Could not create X500 Principle data"); } BigInteger serialNumber = ((ASN1Integer) issuerAndSerialNumber.getObjectAt(1)).getValue(); - signerCertificate = null; // signer cert should always be the first in the list. for (Object element : certificateChain) { X509Certificate certificate = (X509Certificate) element; @@ -381,7 +381,7 @@ private ASN1Sequence parseCertificateData(byte[] cmsData, ASN1Sequence signedDat * @return ASN1Sequence * @throws SignatureIntegrityException error parsing certificate dat. */ - protected ASN1Sequence captureSignedData(byte[] cmsData) + public ASN1Sequence captureSignedData(byte[] cmsData) throws SignatureIntegrityException { ASN1Sequence cmsSequence = buildASN1Primitive(cmsData); if (cmsSequence == null || cmsSequence.getObjectAt(0) == null) { @@ -520,10 +520,8 @@ protected void validateDocument() throws SignatureIntegrityException { try { String provider = signatureDictionary.getFilter().getName(); - messageDigestAlgorithm = AlgorithmIdentifier.getDigestInstance( - digestAlgorithmIdentifier, provider); - eConMessageDigestAlgorithm = AlgorithmIdentifier.getDigestInstance( - digestAlgorithmIdentifier, provider); + messageDigestAlgorithm = AlgorithmIdentifier.getDigestInstance(digestAlgorithmIdentifier, provider); + eConMessageDigestAlgorithm = AlgorithmIdentifier.getDigestInstance(digestAlgorithmIdentifier, provider); signature = createSignature(signerCertificate.getPublicKey(), provider, signatureAlgorithmIdentifier, digestAlgorithmIdentifier); @@ -593,7 +591,7 @@ protected void validateDocument() throws SignatureIntegrityException { logger.finest("Encapsulated data verified: " + verifyEncContentInfoData); } // verify the attributes. - if ((encapsulatedDigestCheck || nonEncapsulatedDigestCheck) && verifyEncContentInfoData) { + if (encapsulatedDigestCheck && nonEncapsulatedDigestCheck && verifyEncContentInfoData) { isSignedDataModified = false; } } else { diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/DocumentSigner.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/DocumentSigner.java new file mode 100644 index 000000000..9192b5d79 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/DocumentSigner.java @@ -0,0 +1,185 @@ +package org.icepdf.core.pobjects.acroform.signature; + +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.operator.OperatorCreationException; +import org.icepdf.core.io.CountingOutputStream; +import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; +import org.icepdf.core.pobjects.security.SecurityManager; +import org.icepdf.core.pobjects.structure.CrossReferenceRoot; +import org.icepdf.core.pobjects.structure.exceptions.CrossReferenceStateException; +import org.icepdf.core.pobjects.structure.exceptions.ObjectStateException; +import org.icepdf.core.util.Library; +import org.icepdf.core.util.updater.writeables.BaseWriter; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * DocumentSigner does the awkward task of populating a SignatureDictionary's /content and /ByteRange entries with + * valid values. The updated SignatureDictionary is inserted back into the file using he same byte footprint but + * contains a singed digest and respective offset of the signed content. + */ +public class DocumentSigner { + + public static int PLACEHOLDER_PADDING_LENGTH = 30000; + public static int PLACEHOLDER_BYTE_OFFSET_LENGTH = 9; + + /** + * The given Document instance will be singed using signatureDictionary location and written to the specified + * output stream. + * + * @param document document contents to be signed + * @param outputFile output file for singed document output + * @param signatureDictionary dictionary to update signer information + * @throws IOException + * @throws CrossReferenceStateException + * @throws ObjectStateException + * @throws UnrecoverableKeyException + * @throws CertificateException + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + * @throws OperatorCreationException + * @throws CMSException + */ + public static void signDocument(Document document, File outputFile, SignatureDictionary signatureDictionary) + throws IOException, CrossReferenceStateException, ObjectStateException, UnrecoverableKeyException, + CertificateException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException, CMSException { + try (final RandomAccessFile raf = new RandomAccessFile(outputFile, "rw");) { + Library library = document.getCatalog().getLibrary(); + int signatureDictionaryOffset = library.getOffset(signatureDictionary.getPObjectReference()); + + StateManager stateManager = document.getStateManager(); + SecurityManager securityManager = document.getSecurityManager(); + CrossReferenceRoot crossReferenceRoot = stateManager.getCrossReferenceRoot(); + + // write out the securityDictionary, so we can make the necessary edits for setting up signing + String rawSignatureDiciontary = writeSignatureDictionary(crossReferenceRoot, securityManager, + signatureDictionary); + int signatureDictionaryLength = rawSignatureDiciontary.length(); + + // figure out byte offset around the content hex string + final FileChannel fc = raf.getChannel(); + fc.position(0); + long fileLength = fc.size(); + + // find byte offset of the start of content hex string + int firstStart = 0; + String contents = "/Contents <"; + int firstOffset = signatureDictionaryOffset + rawSignatureDiciontary.indexOf(contents) + contents.length(); + int secondStart = firstOffset + PLACEHOLDER_PADDING_LENGTH; + int secondOffset = (int) fileLength - secondStart; // just awkward, but should 32bit max. + + // find length of the new array. + List byteRangeArray = List.of(firstStart, firstOffset, secondStart, secondOffset); + String byteRangeDump = writeByteOffsets(crossReferenceRoot, securityManager, byteRangeArray); + // adjust the second start, we will make sure the padding zeros on the /contents hex string adjust + // accordingly + int byteRangeDelta = byteRangeDump.length() - PLACEHOLDER_BYTE_OFFSET_LENGTH; + secondStart -= byteRangeDelta; + secondOffset = (int) fileLength - secondStart; + byteRangeArray = List.of(firstStart, firstOffset, secondStart, secondOffset); + byteRangeDump = writeByteOffsets(crossReferenceRoot, securityManager, byteRangeArray); + // update /ByteRange + + rawSignatureDiciontary = rawSignatureDiciontary.replace("/ByteRange [0 0 0 0]", + "/ByteRange " + byteRangeDump); + + // update /contents with adjusted length for byteRange offset + Pattern pattern = Pattern.compile("/Contents <([A-Fa-f0-9]+)>"); + Matcher matcher = pattern.matcher(rawSignatureDiciontary); + rawSignatureDiciontary = + matcher.replaceFirst("/Contents <" + generateContentsPlaceholder(byteRangeDelta) + ">"); + + // write the altered signature dictionary + fc.position(signatureDictionaryOffset); + fc.write(ByteBuffer.wrap(rawSignatureDiciontary.getBytes())); + + // digest the file creating the content signature + ByteBuffer preContent = ByteBuffer.allocateDirect(firstOffset); + ByteBuffer postContent = ByteBuffer.allocateDirect(secondOffset); + fc.position(firstStart); + fc.read(preContent); + fc.position(secondStart); + fc.read(postContent); + byte[] combined = new byte[preContent.limit() + postContent.limit()]; + ByteBuffer buffer = ByteBuffer.wrap(combined); + preContent.flip(); + postContent.flip(); + buffer.put(preContent); + buffer.put(postContent); + + byte[] signature = signatureDictionary.getSignedData(combined); + String hexContent = HexStringObject.encodeHexString(signature); + if (hexContent.length() < PLACEHOLDER_PADDING_LENGTH) { + int padding = PLACEHOLDER_PADDING_LENGTH - byteRangeDelta - hexContent.length(); + hexContent = hexContent + "0".repeat(Math.max(0, padding)); + } else { + throw new IllegalStateException("signature content is larger than placeholder"); + } + // update /contents with signature + rawSignatureDiciontary = matcher.replaceFirst("/Contents <" + hexContent + ">"); + + // write the altered signature dictionary + fc.position(signatureDictionaryOffset); + int count = fc.write(ByteBuffer.wrap(rawSignatureDiciontary.getBytes())); + + // make sure the object length didn't change + if (count != signatureDictionaryLength) { + throw new IllegalStateException("Signature dictionary length change original " + count + + " new " + signatureDictionaryLength); + } + + } + } + + public static String writeSignatureDictionary(CrossReferenceRoot crossReferenceRoot, + SecurityManager securityManager, + SignatureDictionary signatureDictionary) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CountingOutputStream objectOutput = new CountingOutputStream(byteArrayOutputStream); + BaseWriter writer = new BaseWriter(crossReferenceRoot, securityManager, objectOutput, 0l); + writer.initializeWriters(); + writer.writePObject(new PObject(signatureDictionary, signatureDictionary.getPObjectReference())); + String objectDump = byteArrayOutputStream.toString(StandardCharsets.UTF_8); + objectOutput.close(); + return objectDump; + } + + public static String writeByteOffsets(CrossReferenceRoot crossReferenceRoot, SecurityManager securityManager, + List offsets) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CountingOutputStream objectOutput = new CountingOutputStream(byteArrayOutputStream); + BaseWriter writer = new BaseWriter(crossReferenceRoot, securityManager, objectOutput, 0l); + writer.initializeWriters(); + writer.writeValue(new PObject(offsets, new Reference(1, 0)), objectOutput); + String objectDump = byteArrayOutputStream.toString(StandardCharsets.UTF_8); + objectOutput.close(); + return objectDump; + } + + public static String generateContentsPlaceholder() { + return generateContentsPlaceholder(0); + } + + public static String generateContentsPlaceholder(int reductionAdjustment) { + int capacity = PLACEHOLDER_PADDING_LENGTH - reductionAdjustment; + StringBuilder paddedZeros = new StringBuilder(capacity); + paddedZeros.append("0".repeat(Math.max(0, capacity))); + return paddedZeros.toString(); + } + + +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs1Validator.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs1Validator.java index 3984ffd17..7d2612722 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs1Validator.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs1Validator.java @@ -49,13 +49,13 @@ public Pkcs1Validator(SignatureFieldDictionary signatureFieldDictionary) throws public void init() throws SignatureIntegrityException { SignatureDictionary signatureDictionary = signatureFieldDictionary.getSignatureDictionary(); announceSignatureType(signatureDictionary); - // start the decode of the raw type. + // start decode of the raw type. StringObject stringObject = signatureDictionary.getContents(); - // make sure we don't loose any bytes converting the string in the raw. + // make sure we don't lose any bytes converting the string in the raw. byte[] cmsData = Utils.convertByteCharSequenceToByteArray(stringObject.getLiteralString()); // get the certificate stringObject = signatureDictionary.getCertString(); - // make sure we don't loose any bytes converting the string in the raw. + // make sure we don't lose any bytes converting the string in the raw. byte[] certsKey = Utils.convertByteCharSequenceToByteArray(stringObject.getLiteralString()); try { diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Generator.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Generator.java new file mode 100644 index 000000000..0a67c1e80 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Generator.java @@ -0,0 +1,36 @@ +package org.icepdf.core.pobjects.acroform.signature; + +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/** + * Pkcs7Generator is a utility class for creating a PKCS7 signature. + */ +public class Pkcs7Generator { + + private CMSSignedDataGenerator signedDataGenerator; + + public Pkcs7Generator() { + } + + public CMSSignedDataGenerator createSignedDataGenerator(String algorithmName, X509Certificate[] certs, + PrivateKey privateKey) throws CertificateEncodingException, OperatorCreationException, CMSException { + signedDataGenerator = new CMSSignedDataGenerator(); + X509Certificate cert = certs[0]; + ContentSigner sha1Signer = new JcaContentSignerBuilder(algorithmName).build(privateKey); + signedDataGenerator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert)); + signedDataGenerator.addCertificates(new JcaCertStore(Arrays.asList(certs))); + return signedDataGenerator; + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Validator.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Validator.java index 46af9691f..eff10f314 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Validator.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/Pkcs7Validator.java @@ -49,7 +49,7 @@ public void init() throws SignatureIntegrityException { // get the signature bytes. HexStringObject hexStringObject = signatureDictionary.getContents(); - // make sure we don't loose any bytes converting the string in the raw. + // make sure we don't lose any bytes converting the string in the raw. byte[] cmsData = Utils.convertByteCharSequenceToByteArray(hexStringObject.getLiteralString()); // Signed-data content type -- start of parsing diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/SignatureValidator.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/SignatureValidator.java index 057ae3a47..0544de079 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/SignatureValidator.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/SignatureValidator.java @@ -45,9 +45,9 @@ public interface SignatureValidator { boolean isSignedDataModified(); /** - * Indicates that data after the signature definition has been been modified. This is most likely do to another - * signature being added to the document or some form or page manipulation. However it is possible that - * an major update has been appended to the document. + * Indicates that data after the signature definition has been modified. This is most likely do to another + * signature being added to the document or some form or page manipulation. However, it is possible that + * a major update has been appended to the document. * * @return true if the document has been modified outside the byte range of the signature. */ @@ -62,7 +62,7 @@ public interface SignatureValidator { boolean isSignaturesCoverDocumentLength(); /** - * Sets the signaturesCoverDocumentLength param to indicate that all signatures have been check and cover + * Sets the signaturesCoverDocumentLength param to indicate that all signatures have been checked and cover * all the bytes in the document. * * @param signaturesCoverDocumentLength true if signatures covers document length. @@ -86,7 +86,7 @@ public interface SignatureValidator { /** * Indicates the signature was self singed and the certificate can not be trusted. * - * @return true if self signed, false otherwise. + * @return true if self-signed, false otherwise. */ boolean isSelfSigned(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureAppearanceCallback.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureAppearanceCallback.java new file mode 100644 index 000000000..fcfbcdfda --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureAppearanceCallback.java @@ -0,0 +1,41 @@ +package org.icepdf.core.pobjects.acroform.signature.appearance; + +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; + +import java.awt.geom.AffineTransform; + +/** + * Interface for custom appearance streams defined by a user or organization. + */ +public interface SignatureAppearanceCallback { + + /** + * Set the SignatureAppearanceModel for the callback. The model is used to store appearance properties needed + * to build out the appearance stream. The model just be set before the create or remove methods are called. + * + * @param signatureAppearanceModel appearance model for the callback + */ + void setSignatureAppearanceModel(T signatureAppearanceModel); + + /** + * Create appearance stream for the given SignatureWidgetAnnotation. The appearance must be associated with + * the SignatureWidgetAnnotation and all new objects registered with the StateManager + * + * @param signatureWidgetAnnotation annotation that created appearance stream will be associated with + * @param pageSpace page space transform for the annotation + * @param isNew true if annotation is considered new and should be added to state manager + */ + void createAppearanceStream(SignatureWidgetAnnotation signatureWidgetAnnotation, AffineTransform pageSpace, + boolean isNew); + + /** + * Remove appearance stream for the given SignatureWidgetAnnotation. Clean up any resources or StateManager state + * associated with the SignatureWidgetAnnotation. + * + * @param signatureWidgetAnnotation + * @param pageSpace + * @param isNew + */ + void removeAppearanceStream(SignatureWidgetAnnotation signatureWidgetAnnotation, AffineTransform pageSpace, + boolean isNew); +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureAppearanceModel.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureAppearanceModel.java new file mode 100644 index 000000000..ff8f9890b --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureAppearanceModel.java @@ -0,0 +1,9 @@ +package org.icepdf.core.pobjects.acroform.signature.appearance; + +/** + * SignatureAppearanceModel is used to store appearance properties needed to build out the appearance stream. This + * model is used by the SignatureAppearanceCallback to build out the appearance stream for a SignatureWidgetAnnotation. + */ +public interface SignatureAppearanceModel { + +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureType.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureType.java new file mode 100644 index 000000000..d4ebe94f3 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/appearance/SignatureType.java @@ -0,0 +1,10 @@ +package org.icepdf.core.pobjects.acroform.signature.appearance; + +/** + * Signatures can be defined two different ways. The first type, Signer is intended for multiple signatures + * on a PDF document, probably the most typical use. The second is a Certifier and is intended to be the only + * signature attached to the document or in some cases the primary signature if more follow. + */ +public enum SignatureType { + SIGNER, CERTIFIER +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/ConsoleCallbackHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/ConsoleCallbackHandler.java new file mode 100644 index 000000000..4784de2f4 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/ConsoleCallbackHandler.java @@ -0,0 +1,60 @@ +package org.icepdf.core.pobjects.acroform.signature.handlers; + +import javax.security.auth.callback.*; +import java.io.Console; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implementation of the CallbackHandler interface that handles different types of callbacks + * and interacts with the user through the console for input or output when working with KeyStores. + */ +public class ConsoleCallbackHandler implements CallbackHandler { + + private static final Logger logger = Logger.getLogger(ConsoleCallbackHandler.class.getName()); + + public ConsoleCallbackHandler() { + if (System.console() == null) { + throw new UnsupportedOperationException("Console not available."); + } + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + Console con = System.console(); + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback pc = (PasswordCallback) callback; + String prompt = pc.getPrompt(); + prompt = prompt != null && !prompt.isEmpty() ? prompt : "PIN"; + con.format("%s: ", prompt); + logger.log(Level.INFO, + "Password callback with prompt: {0}", + prompt); + char[] password = con.readPassword(); + pc.setPassword(password); + pc.setPassword("changeit".toCharArray()); + } else if (callback instanceof TextOutputCallback) { + TextOutputCallback tc = (TextOutputCallback) callback; + con.format("%s", tc.getMessage()); + logger.log(Level.INFO, + "TextOutputCallback type {0} message: {1}", + new Object[]{tc.getMessageType(), tc.getMessage()}); + throw new UnsupportedCallbackException(callback); + } else if (callback instanceof NameCallback) { + NameCallback nc = (NameCallback) callback; + String prompt = nc.getPrompt(); + prompt = prompt != null && !prompt.isEmpty() ? prompt : "Name"; + String defaultName = nc.getDefaultName(); + throw new UnsupportedCallbackException(callback); + } else { + logger.log(Level.WARNING, + "Unknown callback type {0}", + callback.getClass().getName()); + throw new UnsupportedCallbackException(callback); + } + } + } + +} \ No newline at end of file diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/PasswordCallbackHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/PasswordCallbackHandler.java new file mode 100644 index 000000000..f13c09b2f --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/PasswordCallbackHandler.java @@ -0,0 +1,38 @@ +package org.icepdf.core.pobjects.acroform.signature.handlers; + +import javax.security.auth.callback.CallbackHandler; + +/** + * PasswordCallbackHandler is a simple implementation of the CallbackHandler interface that is used to provide a + * password + * to the SignatureValidator implementations. + */ +public abstract class PasswordCallbackHandler implements CallbackHandler { + + protected char[] password; + + /** + * Create a new PasswordCallbackHandler with the given password. + * + * @param password + */ + public PasswordCallbackHandler(String password) { + this.password = password.toCharArray(); + } + + /** + * Create a new PasswordCallbackHandler with the given password. + * @param password + */ + public PasswordCallbackHandler(char[] password) { + this.password = password; + } + + /** + * Get the password as a char array. + * @return + */ + protected char[] getPassword() { + return password; + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/Pkcs11SignerHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/Pkcs11SignerHandler.java new file mode 100644 index 000000000..a21c53230 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/Pkcs11SignerHandler.java @@ -0,0 +1,93 @@ +package org.icepdf.core.pobjects.acroform.signature.handlers; + +import java.math.BigInteger; +import java.security.*; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + *

Pkcs11SignerHandler tries to do some of the boilerplate work to set up a Pkcs#11 provider. The configuration file + * must point at a shared-object library (.so on linux) and dynamic-link library (.dll on windows or .dylib on macOS). + * Consult the vendors of the PKCS#11 cryptographic API documentation for more information. + * More information can be found at + * pkcs11-reference-guide + *

+ * A sample config file might be as follows: + *
+ *     # /opt/bar/cfg/pkcs11.cfg
+ *     name = vendor_name
+ *     library = /opt/foo/lib/libPKCS11.so
+ *     slot = 0
+ * 
+ * + *

The SunPKCS11 provider, in contrast to most other providers, does not implement cryptographic algorithms itself. + * Instead, it acts as a bridge between the Java JCA and JCE APIs and the native PKCS#11 cryptographic API, translating + * the calls and conventions between the two. + *

+ * + *

This means that Java applications calling standard JCA and JCE APIs can, without modification, take advantage of + * algorithms offered by the underlying PKCS#11 implementations, such as, for example,

+ *
    + *
  • Cryptographic smartcards,
  • + *
  • Hardware cryptographic accelerators, and
  • + *
  • High performance software implementations.
  • + *
+ *

For additional debugging info, users can start or restart the Java processes with one of the following + * options:

+ *
    + *
  • For general SunPKCS11 provider debugging info: -Djava.security.debug=sunpkcs11
  • + *
  • For PKCS#11 keystore specific debugging info: -Djava.security.debug=pkcs11keystore

  • + *
+ */ +public class Pkcs11SignerHandler extends SignerHandler { + + private static final Logger logger = Logger.getLogger(SimplePasswordCallbackHandler.class.getName()); + + private final String providerConfig; + private final BigInteger certSerial; + + public Pkcs11SignerHandler(String providerConfig, BigInteger certSerial, PasswordCallbackHandler callbackHandler) { + super(null, callbackHandler); + this.providerConfig = providerConfig; + this.certSerial = certSerial; + } + + @Override + public KeyStore buildKeyStore() throws KeyStoreException { + Provider provider = Security.getProvider("SunPKCS11"); + provider = provider.configure(this.providerConfig); + Security.addProvider(provider); + logger.log(Level.INFO, "buildKeyStore, created SunPKCS11 provider"); + KeyStore.CallbackHandlerProtection chp = new KeyStore.CallbackHandlerProtection(this.callbackHandler); + KeyStore.Builder builder = KeyStore.Builder.newInstance("PKCS11", provider, chp); + keystore = builder.getKeyStore(); + return keystore; + } + + @Override + protected PrivateKey getPrivateKey() throws KeyStoreException, UnrecoverableKeyException, + NoSuchAlgorithmException { + logger.log(Level.INFO, "search for"); + certAlias = getAliasByCertificateSerialNumber(keystore, certSerial); + logger.log(Level.INFO, "buildKeyStore, retrieved cert alias: " + certAlias); + logger.log(Level.INFO, "buildKeyStore, should use pin/password from callbackHandler"); + return (PrivateKey) keystore.getKey(certAlias, null); // pulls password from callbackHandler + } + + private String getAliasByCertificateSerialNumber(KeyStore keyStore, BigInteger certSerial) throws KeyStoreException { + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + logger.log(Level.INFO, "Alias: {0}", alias); + X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); + logger.log(Level.INFO, " Certificate Serial: {0}", cert.getSerialNumber().toString(16)); + if (certSerial.equals(cert.getSerialNumber())) { + return alias; + } + } + throw new IllegalStateException("No certificate number " + certSerial.toString(16) + " in KeyStore."); + } + +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/Pkcs12SignerHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/Pkcs12SignerHandler.java new file mode 100644 index 000000000..b40dee8cf --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/Pkcs12SignerHandler.java @@ -0,0 +1,38 @@ +package org.icepdf.core.pobjects.acroform.signature.handlers; + +import java.io.File; +import java.security.*; + +/** + * This class implements the SignerHandler interface and provides the functionality to sign data using PKCS12 format. + */ +public class Pkcs12SignerHandler extends SignerHandler { + + private final File keystoreFile; + + /** + * Constructs a Pkcs12SignerHandler object with the provided parameters. + * + * @param keyStore the PKCS12 keystore file + * @param certAlias the alias of the certificate in the keystore + * @param callbackHandler the callback handler to retrieve the password for the keystore + */ + public Pkcs12SignerHandler(File keyStore, String certAlias, PasswordCallbackHandler callbackHandler) { + super(certAlias, callbackHandler); + this.keystoreFile = keyStore; + } + + @Override + public KeyStore buildKeyStore() throws KeyStoreException { + KeyStore.CallbackHandlerProtection chp = new KeyStore.CallbackHandlerProtection(callbackHandler); + KeyStore.Builder builder = KeyStore.Builder.newInstance(keystoreFile, chp); + keystore = builder.getKeyStore(); + return keystore; + } + + @Override + protected PrivateKey getPrivateKey() throws KeyStoreException, UnrecoverableKeyException, + NoSuchAlgorithmException { + return (PrivateKey) keystore.getKey(certAlias, callbackHandler.getPassword()); + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/SignerHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/SignerHandler.java new file mode 100644 index 000000000..c5b882b28 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/SignerHandler.java @@ -0,0 +1,75 @@ +package org.icepdf.core.pobjects.acroform.signature.handlers; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.operator.OperatorCreationException; +import org.icepdf.core.pobjects.acroform.signature.Pkcs7Generator; + +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Signer handles the setup and signing work to generate a PKCS7 signed hash for the given data. Implementing + * classes must implement the abstract methods to create a keystore with access to a private key used for signing. + */ +public abstract class SignerHandler { + + protected static final String algorithm = "SHA256WithRSA"; + + protected String certAlias; + protected KeyStore keystore; + protected PasswordCallbackHandler callbackHandler; + + public SignerHandler(String certAlias, PasswordCallbackHandler callbackHandler) { + this.certAlias = certAlias; + this.callbackHandler = callbackHandler; + } + + public void setCertAlias(String certAlias) { + this.certAlias = certAlias; + } + + public abstract KeyStore buildKeyStore() throws KeyStoreException; + + protected abstract PrivateKey getPrivateKey() throws KeyStoreException, UnrecoverableKeyException, + NoSuchAlgorithmException; + + public X509Certificate getCertificate() throws KeyStoreException { + if (keystore == null) { + keystore = buildKeyStore(); + } + return (X509Certificate) keystore.getCertificate(certAlias); + } + + public X509Certificate getCertificate(String alias) throws KeyStoreException { + if (keystore == null) { + keystore = buildKeyStore(); + } + return (X509Certificate) keystore.getCertificate(alias); + } + + public byte[] signData(byte[] data) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, + CertificateException, OperatorCreationException, CMSException, IOException { + + if (keystore == null) { + keystore = buildKeyStore(); + } + PrivateKey privateKey = getPrivateKey(); + + X509Certificate certificate = (X509Certificate) keystore.getCertificate(certAlias); + + CMSSignedDataGenerator signedDataGenerator = new Pkcs7Generator() + .createSignedDataGenerator(algorithm, new X509Certificate[]{certificate}, privateKey); + + CMSProcessableByteArray message = + new CMSProcessableByteArray(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), data); + CMSSignedData signedData = signedDataGenerator.generate(message, false); + return signedData.getEncoded(); + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/SimplePasswordCallbackHandler.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/SimplePasswordCallbackHandler.java new file mode 100644 index 000000000..d8ebb7afb --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/handlers/SimplePasswordCallbackHandler.java @@ -0,0 +1,43 @@ +package org.icepdf.core.pobjects.acroform.signature.handlers; + +import javax.security.auth.callback.*; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * PasswordCallbackHandler is a simple implementation of the CallbackHandler interface that is used to provide a + * password to the SignatureValidator implementations. + */ +public class SimplePasswordCallbackHandler extends PasswordCallbackHandler { + + private static final Logger logger = Logger.getLogger(SimplePasswordCallbackHandler.class.getName()); + + public SimplePasswordCallbackHandler(String password) { + super(password); + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback pc = (PasswordCallback) callback; + pc.setPassword(password); + } else if (callback instanceof TextOutputCallback) { + TextOutputCallback tc = (TextOutputCallback) callback; + logger.log(Level.INFO, + "TextOutputCallback type {0} message: {1}", + new Object[]{tc.getMessageType(), tc.getMessage()}); + throw new UnsupportedCallbackException(callback); + } else if (callback instanceof NameCallback) { + throw new UnsupportedCallbackException(callback); + } else { + logger.log(Level.WARNING, + "Unknown callback type {0}", + callback.getClass().getName()); + throw new UnsupportedCallbackException(callback); + } + } + } + +} \ No newline at end of file diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/utils/SignatureUtilities.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/utils/SignatureUtilities.java new file mode 100644 index 000000000..c60b8c450 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/acroform/signature/utils/SignatureUtilities.java @@ -0,0 +1,105 @@ +/* + * Copyright 2006-2019 ICEsoft Technologies Canada Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an "AS + * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.icepdf.core.pobjects.acroform.signature.utils; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; + +import javax.imageio.ImageIO; +import javax.security.auth.x500.X500Principal; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility of commonly used signature related algorithms. + */ +public class SignatureUtilities { + + private static final Logger logger = + Logger.getLogger(SignatureUtilities.class.toString()); + + /** + * Parse out a known data element from an X500Name. + * + * @param rdName name to parse value from. + * @param commonCode BCStyle name . + * @return BCStyle name value, null if the BCStyle name was not found. + */ + public static String parseRelativeDistinguishedName(X500Name rdName, ASN1ObjectIdentifier commonCode) { + RDN[] rdns = rdName.getRDNs(commonCode); + if (rdns != null && rdns.length > 0 && rdns[0].getFirst() != null) { + return rdns[0].getFirst().getValue().toString(); + } + return null; + } + + /** + * Populate signature dictionary with values from the certificate + * + * @param signatureDictionary dictionary to populate + * @param certificate cert to extract values from + */ + public static void updateSignatureDictionary(SignatureDictionary signatureDictionary, X509Certificate certificate) { + X500Principal principal = certificate.getSubjectX500Principal(); + X500Name x500name = new X500Name(principal.getName()); + // Set up dictionary using certificate values. + // https://javadoc.io/static/org.bouncycastle/bcprov-jdk15on/1.70/org/bouncycastle/asn1/x500/style/BCStyle.html + if (x500name.getRDNs() != null) { + String commonName = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.CN); + if (commonName != null) { + signatureDictionary.setName(commonName); + } + String email = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.EmailAddress); + if (email != null) { + signatureDictionary.setContactInfo(email); + } + ArrayList location = new ArrayList<>(2); + String state = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.ST); + if (state != null) { + location.add(state); + } + String country = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.C); + if (country != null) { + location.add(country); + } + if (!location.isEmpty()) { + signatureDictionary.setLocation(String.join(", ", location)); + } + } else { + throw new IllegalStateException("Certificate has no DRNs data"); + } + } + + public static BufferedImage loadSignatureImage(String imagePath) { + if (imagePath == null || imagePath.isEmpty()) { + return null; + } + try { + return ImageIO.read(new File(imagePath)); + } catch (IOException e) { + logger.log(Level.WARNING, "Error loading signature image", e); + } + return null; + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AbstractWidgetAnnotation.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AbstractWidgetAnnotation.java index 44f364346..4b67f7981 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AbstractWidgetAnnotation.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AbstractWidgetAnnotation.java @@ -16,9 +16,7 @@ package org.icepdf.core.pobjects.annotations; -import org.icepdf.core.pobjects.DictionaryEntries; -import org.icepdf.core.pobjects.Name; -import org.icepdf.core.pobjects.Resources; +import org.icepdf.core.pobjects.*; import org.icepdf.core.pobjects.acroform.FieldDictionary; import org.icepdf.core.pobjects.acroform.InteractiveForm; import org.icepdf.core.util.ColorUtil; @@ -115,6 +113,23 @@ public void init() throws InterruptedException { // todo check if we have content value but no appearance stream. } + protected static DictionaryEntries createCommonFieldDictionary(Name fieldType, Rectangle rect) { + DictionaryEntries entries = new DictionaryEntries(); + // set default link annotation values. + entries.put(Dictionary.TYPE_KEY, Annotation.TYPE_VALUE); + entries.put(Dictionary.SUBTYPE_KEY, Annotation.SUBTYPE_WIDGET); + entries.put(FieldDictionary.FT_KEY, fieldType); + entries.put(Annotation.FLAG_KEY, 4); + // coordinates + if (rect != null) { + entries.put(Annotation.RECTANGLE_KEY, + PRectangle.getPRectangleVector(rect)); + } else { + entries.put(Annotation.RECTANGLE_KEY, new Rectangle(10, 10, 50, 100)); + } + return entries; + } + public abstract void reset(); @Override @@ -134,8 +149,7 @@ protected void renderAppearanceStream(Graphics2D g, float rotation, float zoom) } // check the highlight widgetAnnotation field and if true we draw a light background colour to mark // the widgets on a page. - if (enableHighlightedWidget && - !(getFieldDictionary() != null && getFieldDictionary().isReadOnly())) { + if (enableHighlightedWidget) { AffineTransform preHighLightTransform = g.getTransform(); g.setColor(highlightColor); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, highlightAlpha)); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/Annotation.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/Annotation.java index b497dbec6..d1fd66ed2 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/Annotation.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/Annotation.java @@ -15,13 +15,14 @@ */ package org.icepdf.core.pobjects.annotations; -import org.icepdf.core.pobjects.Dictionary; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.Dictionary; import org.icepdf.core.pobjects.acroform.FieldDictionary; import org.icepdf.core.pobjects.acroform.FieldDictionaryFactory; import org.icepdf.core.pobjects.actions.Action; import org.icepdf.core.pobjects.graphics.Shapes; import org.icepdf.core.pobjects.security.SecurityManager; +import org.icepdf.core.util.Defs; import org.icepdf.core.util.GraphicsRenderingHints; import org.icepdf.core.util.Library; import org.icepdf.core.util.Utils; @@ -30,8 +31,8 @@ import java.awt.geom.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; -import java.util.List; import java.util.*; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -525,7 +526,12 @@ public abstract class Annotation extends Dictionary { * Debug flag to turn off appearance stream compression for easier * human file reading. */ - protected static boolean compressAppearanceStream = true; + protected static boolean compressAppearanceStream; + + static { + compressAppearanceStream = Defs.booleanProperty("org.icepdf.core.annotation.compressStream", true); + } + protected final HashMap appearances = new HashMap<>(3); protected Name currentAppearance; @@ -633,6 +639,10 @@ public static void setCompressAppearanceStream(boolean compressAppearanceStream) Annotation.compressAppearanceStream = compressAppearanceStream; } + public static boolean isCompressAppearanceStream() { + return compressAppearanceStream; + } + public synchronized void init() throws InterruptedException { super.init(); // type of Annotation diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AnnotationFactory.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AnnotationFactory.java index 94bedce69..c9145bfae 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AnnotationFactory.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/AnnotationFactory.java @@ -16,6 +16,7 @@ package org.icepdf.core.pobjects.annotations; import org.icepdf.core.pobjects.Name; +import org.icepdf.core.pobjects.acroform.FieldDictionaryFactory; import org.icepdf.core.util.Library; import java.awt.*; @@ -35,7 +36,7 @@ public class AnnotationFactory { /** * Creates a new Annotation object using properties from the annotationState - * paramater. If no annotaitonState is provided a LinkAnnotation is returned + * parameter. If no annotationState is provided a LinkAnnotation is returned * with a black border. The rect specifies where the annotation should * be located in user space. *
@@ -80,4 +81,28 @@ else if (TextMarkupAnnotation.isTextMarkupAnnotation(subType)) { return null; } } + + /** + * Creates a new Widget Annotation object using properties from the annotationState + * parameter. + *
+ * This call adds the new Annotation object to the document library as well + * as the document StateManager. + * + * @param library library to register annotation with + * @param fieldType field type to create + * @param rect bounds of new annotation specified in user space. + * @return new annotation object + */ + public static Annotation buildWidgetAnnotation(Library library, + final Name fieldType, + Rectangle rect) { + // build up a link annotation + if (fieldType.equals(FieldDictionaryFactory.TYPE_SIGNATURE)) { + return SignatureWidgetAnnotation.getInstance(library, rect); + } else { + logger.warning("Unsupported Annotation type. "); + return null; + } + } } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/FreeTextAnnotation.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/FreeTextAnnotation.java index 03db6970b..728878890 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/FreeTextAnnotation.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/FreeTextAnnotation.java @@ -16,12 +16,9 @@ package org.icepdf.core.pobjects.annotations; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.annotations.utils.ContentWriterUtils; import org.icepdf.core.pobjects.fonts.FontFile; -import org.icepdf.core.pobjects.fonts.FontManager; -import org.icepdf.core.pobjects.fonts.zfont.Encoding; import org.icepdf.core.pobjects.graphics.Shapes; -import org.icepdf.core.pobjects.graphics.TextSprite; -import org.icepdf.core.pobjects.graphics.TextState; import org.icepdf.core.pobjects.graphics.commands.*; import org.icepdf.core.util.ColorUtil; import org.icepdf.core.util.Defs; @@ -37,8 +34,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static org.icepdf.core.pobjects.fonts.Font.SIMPLE_FORMAT; - /** * A free text annotation (PDF 1.3) displays text directly on the page. Unlike * an ordinary text annotation (see 12.5.6.4, "Text Annotations"), a free text @@ -229,7 +224,7 @@ public class FreeTextAnnotation extends MarkupAnnotation { protected String richText; // appearance properties not to be confused with annotation properties, - // this properties are updated by the UI components and used to regenerate + // these properties are updated by the UI components and used to regenerate // the annotations appearance stream and other needed properties on edits. private String fontName = "Helvetica"; private int fontStyle = Font.PLAIN; @@ -380,28 +375,9 @@ public void resetAppearanceStream(double dx, double dy, AffineTransform pageTran Rectangle2D bbox = appearanceState.getBbox(); bbox.setRect(0, 0, bbox.getWidth(), bbox.getHeight()); - AffineTransform matrix = appearanceState.getMatrix(); - Shapes shapes = appearanceState.getShapes(); - - if (shapes == null) { - shapes = new Shapes(); - appearanceState.setShapes(shapes); - } else { - // remove any previous text - appearanceState.getShapes().getShapes().clear(); - } - - // remove any previous text - shapes.getShapes().clear(); + Shapes shapes = ContentWriterUtils.createAppearanceShapes(appearanceState, INSETS, INSETS); - // setup the space for the AP content stream. - AffineTransform af = new AffineTransform(); - af.scale(1, -1); - af.translate(0, -bbox.getHeight()); - // adjust of the border offset, offset is define in viewer, - // so we can't use the constant because of dependency issues. - af.translate(INSETS, INSETS); - shapes.add(new TransformDrawCmd(af)); + AffineTransform matrix = appearanceState.getMatrix(); // iterate over each line of text painting the strings. if (content == null) { @@ -410,61 +386,11 @@ public void resetAppearanceStream(double dx, double dy, AffineTransform pageTran // create the new font to draw with if (fontFile == null || fontPropertyChanged) { - fontFile = FontManager.getInstance().initialize().getInstance(fontName, 0); - fontFile = fontFile.deriveFont(Encoding.standardEncoding, null); + fontFile = ContentWriterUtils.createFont(fontName); fontPropertyChanged = false; } fontFile = fontFile.deriveFont(fontSize); - TextSprite textSprites = - new TextSprite(fontFile, - SIMPLE_FORMAT, - content.length(), - new AffineTransform(), null); - textSprites.setRMode(TextState.MODE_FILL); - textSprites.setStrokeColor(fontColor); - textSprites.setFontName(EMBEDDED_FONT_NAME.toString()); - textSprites.setFontSize(fontSize); - - // iterate over each line of text painting the strings. - StringBuilder contents = new StringBuilder(content); - - // todo temporary get working until I can get back to calculating max char bounds. - float lineHeight = fontSize; - - float borderOffsetX = borderStyle.getStrokeWidth() / 2 + 1; // 1 pixel padding - float borderOffsetY = borderStyle.getStrokeWidth() / 2; - // is generally going to be zero, and af takes care of the offset for inset. - float advanceX = (float) bbox.getMinX() + borderOffsetX; - float advanceY = (float) bbox.getMinY() + borderOffsetY; - float currentX; - // we don't want to shift the whole line width just the ascent - float currentY = advanceY + lineHeight; - - float lastx = 0; - float newAdvanceX; - char currentChar; - for (int i = 0, max = contents.length(); i < max; i++) { - - currentChar = contents.charAt(i); - - newAdvanceX = (float) fontFile.getAdvance(currentChar).getX(); - currentX = advanceX + lastx; - lastx += newAdvanceX; - - if (!(currentChar == '\n' || currentChar == '\r')) { - textSprites.addText( - currentChar, // cid - EMBEDDED_FONT_NAME, - String.valueOf(currentChar), // unicode value - currentX, currentY, newAdvanceX, 0, 0); - } else { - // move back to start of next line - currentY += lineHeight; - advanceX = (float) bbox.getMinX() + borderOffsetX; - lastx = 0; - } - } BasicStroke stroke; if (strokeType && borderStyle.isStyleDashed()) { stroke = new BasicStroke( @@ -493,11 +419,13 @@ public void resetAppearanceStream(double dx, double dy, AffineTransform pageTran shapes.add(new DrawDrawCmd()); } // actual font. - shapes.add(new ColorDrawCmd(fontColor)); - shapes.add(new TextSpriteDrawCmd(textSprites)); - - shapes.add(new AlphaDrawCmd( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f))); + float borderOffsetX = borderStyle.getStrokeWidth() / 2 + 1; // 1 pixel padding + float borderOffsetY = borderStyle.getStrokeWidth() / 2; + // is generally going to be zero, and af takes care of the offset for inset. + float advanceX = (float) bbox.getMinX() + borderOffsetX; + float advanceY = (float) bbox.getMinY() + borderOffsetY; + ContentWriterUtils.addTextSpritesToShapes(fontFile, advanceX, advanceY, shapes, fontSize, 0, fontColor, + content); // update the appearance stream // create/update the appearance stream of the xObject. @@ -505,76 +433,8 @@ public void resetAppearanceStream(double dx, double dy, AffineTransform pageTran Form form = updateAppearanceStream(shapes, bbox, matrix, PostScriptEncoder.generatePostScript(shapes.getShapes()), isNew); generateExternalGraphicsState(form, opacity); - - if (form != null) { - Rectangle2D formBbox = new Rectangle2D.Float(0, 0, - (float) bbox.getWidth(), (float) bbox.getHeight()); - form.setAppearance(shapes, matrix, formBbox); - stateManager.addChange(new PObject(form, form.getPObjectReference()), isNew); - // update the AP's stream bytes so contents can be written out - form.setRawBytes( - PostScriptEncoder.generatePostScript(shapes.getShapes())); - DictionaryEntries appearanceRefs = new DictionaryEntries(); - appearanceRefs.put(APPEARANCE_STREAM_NORMAL_KEY, form.getPObjectReference()); - entries.put(APPEARANCE_STREAM_KEY, appearanceRefs); - - // compress the form object stream. - if (compressAppearanceStream) { - form.getEntries().put(Stream.FILTER_KEY, new Name("FlateDecode")); - } else { - form.getEntries().remove(Stream.FILTER_KEY); - } - - // create the font - DictionaryEntries fontDictionary = new DictionaryEntries(); - fontDictionary.put(org.icepdf.core.pobjects.fonts.Font.TYPE_KEY, - org.icepdf.core.pobjects.fonts.Font.SUBTYPE_KEY); - fontDictionary.put(org.icepdf.core.pobjects.fonts.Font.SUBTYPE_KEY, - new Name("Type1")); - fontDictionary.put(org.icepdf.core.pobjects.fonts.Font.NAME_KEY, - EMBEDDED_FONT_NAME); - fontDictionary.put(org.icepdf.core.pobjects.fonts.Font.BASEFONT_KEY, - new Name(fontName)); - fontDictionary.put(org.icepdf.core.pobjects.fonts.Font.ENCODING_KEY, - new Name("WinAnsiEncoding")); - fontDictionary.put(new Name("FirstChar"), 32); - fontDictionary.put(new Name("LastChar"), 255); - - org.icepdf.core.pobjects.fonts.Font newFont; - if (form.getResources() == null || - form.getResources().getFont(EMBEDDED_FONT_NAME) == null) { - newFont = new org.icepdf.core.pobjects.fonts.zfont.SimpleFont( - library, fontDictionary); - newFont.setPObjectReference(stateManager.getNewReferenceNumber()); - // create font entry - DictionaryEntries fontResources = new DictionaryEntries(); - fontResources.put(EMBEDDED_FONT_NAME, newFont.getPObjectReference()); - // add the font resource entry. - DictionaryEntries resources = new DictionaryEntries(); - resources.put(new Name("Font"), fontResources); - // and finally add it to the form. - form.getEntries().put(new Name("Resources"), resources); - form.setRawBytes("".getBytes()); - try { - form.init(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } else { - try { - form.init(); - } catch (InterruptedException e) { - logger.log(Level.WARNING, "Could not initialized FreeTexttAnnotation", e); - } - newFont = form.getResources().getFont(EMBEDDED_FONT_NAME); - Reference reference = newFont.getPObjectReference(); - newFont = new org.icepdf.core.pobjects.fonts.zfont.SimpleFont(library, fontDictionary); - newFont.setPObjectReference(reference); - } - // update hard reference to state manager and weak library reference. - stateManager.addChange(new PObject(newFont, newFont.getPObjectReference()), isNew); - library.addObject(newFont, newFont.getPObjectReference()); - } + ContentWriterUtils.setAppearance(this, form, appearanceState, stateManager, isNew); + form.addFontResource(ContentWriterUtils.createDefaultFontDictionary(fontName)); // build out a few backwards compatible strings. StringBuilder dsString = new StringBuilder("font-size:") @@ -781,7 +641,8 @@ private void parseDefaultStylingString() { } public static final String BODY_START = - ""; public static final String BODY_END = ""; diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/SignatureWidgetAnnotation.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/SignatureWidgetAnnotation.java index 83bb1e71c..b6a23955a 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/SignatureWidgetAnnotation.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/annotations/SignatureWidgetAnnotation.java @@ -1,16 +1,18 @@ package org.icepdf.core.pobjects.annotations; -import org.icepdf.core.pobjects.DictionaryEntries; -import org.icepdf.core.pobjects.acroform.FieldDictionary; -import org.icepdf.core.pobjects.acroform.SignatureDictionary; -import org.icepdf.core.pobjects.acroform.SignatureFieldDictionary; -import org.icepdf.core.pobjects.acroform.SignatureHandler; +import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.acroform.*; import org.icepdf.core.pobjects.acroform.signature.SignatureValidator; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureAppearanceCallback; import org.icepdf.core.util.Library; +import java.awt.*; import java.awt.geom.AffineTransform; +import java.util.Date; import java.util.logging.Logger; +import static org.icepdf.core.pobjects.acroform.SignatureDictionary.V_KEY; + /** * A digital signature (PDF 1.3) may be used to authenticate the identity of a user and the document's contents. It * stores information about the signer and the state of the document when it was signed. The signature may be purely @@ -39,13 +41,18 @@ public class SignatureWidgetAnnotation extends AbstractWidgetAnnotation drawCmds) { // setup an affine transform if (drawCmd instanceof TransformDrawCmd) { AffineTransform af = ((TransformDrawCmd) drawCmd).getAffineTransform(); - postScript.append(af.getScaleX()).append(SPACE) - .append(af.getShearX()).append(SPACE) - .append(af.getShearY()).append(SPACE) - .append(af.getScaleY()).append(SPACE) - .append(af.getTranslateX()).append(SPACE) - .append(af.getTranslateY()).append(SPACE) + postScript.append(roundCoordinate(af.getScaleX())).append(SPACE) + .append(roundCoordinate(af.getShearX())).append(SPACE) + .append(roundCoordinate(af.getShearY())).append(SPACE) + .append(roundCoordinate(af.getScaleY())).append(SPACE) + .append(roundCoordinate(af.getTranslateX())).append(SPACE) + .append(roundCoordinate(af.getTranslateY())).append(SPACE) .append(PdfOps.cm_TOKEN).append(NEWLINE); } else if (drawCmd instanceof TextTransformDrawCmd) { AffineTransform af = ((TransformDrawCmd) drawCmd).getAffineTransform(); - postScript.append(af.getScaleX()).append(SPACE) - .append(af.getShearX()).append(SPACE) - .append(af.getShearY()).append(SPACE) - .append(af.getScaleY()).append(SPACE) - .append(af.getTranslateX()).append(SPACE) - .append(af.getTranslateY()).append(SPACE) + postScript.append(roundCoordinate(af.getScaleX())).append(SPACE) + .append(roundCoordinate(af.getShearX())).append(SPACE) + .append(roundCoordinate(af.getShearY())).append(SPACE) + .append(roundCoordinate(af.getScaleY())).append(SPACE) + .append(roundCoordinate(af.getTranslateX())).append(SPACE) + .append(roundCoordinate(af.getTranslateY())).append(SPACE) .append(PdfOps.Tm_TOKEN).append(NEWLINE); } // reference the colour, we'll decide later if its fill or stroke. @@ -139,8 +140,8 @@ else if (drawCmd instanceof ShapeDrawCmd) { else if (drawCmd instanceof StrokeDrawCmd) { BasicStroke stroke = (BasicStroke) ((StrokeDrawCmd) drawCmd).getStroke(); postScript.append( - // line width - stroke.getLineWidth()).append(SPACE) + // line width + stroke.getLineWidth()).append(SPACE) .append(PdfOps.w_TOKEN).append(SPACE); // dash phase float[] dashes = stroke.getDashArray(); @@ -182,7 +183,7 @@ else if (drawCmd instanceof StrokeDrawCmd) { } // graphics state setup else if (drawCmd instanceof GraphicsStateCmd) { - postScript.append('/') + postScript.append(NAME) .append(((GraphicsStateCmd) drawCmd).getGraphicStateName()).append(SPACE) .append(PdfOps.gs_TOKEN).append(SPACE); } @@ -194,13 +195,13 @@ else if (drawCmd instanceof TextSpriteDrawCmd) { ArrayList glyphTexts = textSprite.getGlyphSprites(); if (glyphTexts.size() > 0) { - // write out stat of text paint + // write out start of text paint postScript.append("1 0 0 -1 ") .append(glyphTexts.get(0).getX()).append(SPACE) .append(glyphTexts.get(0).getY()).append(SPACE).append(PdfOps.Tm_TOKEN).append(NEWLINE); // write out font - postScript.append("/").append(textSprite.getFontName()).append(SPACE) + postScript.append(NAME).append(textSprite.getFontName()).append(SPACE) .append(textSprite.getFontSize()).append(SPACE).append(PdfOps.Tf_TOKEN).append(NEWLINE); // set the colour @@ -239,6 +240,14 @@ else if (drawCmd instanceof TextSpriteDrawCmd) { } postScript.append(PdfOps.ET_TOKEN).append(NEWLINE); } + } else if (drawCmd instanceof PopDrawCmd) { + postScript.append(PdfOps.Q_TOKEN).append(SPACE); + } else if (drawCmd instanceof PushDrawCmd) { + postScript.append(PdfOps.q_TOKEN).append(SPACE); + } else if (drawCmd instanceof ImageDrawCmd) { + ImageDrawCmd imageDrawCmd = (ImageDrawCmd) drawCmd; + Name imageName = imageDrawCmd.getImageName(); + postScript.append(NAME).append(imageName).append(SPACE).append(PdfOps.Do_TOKEN).append(NEWLINE); } } } catch (Exception e) { @@ -250,6 +259,10 @@ else if (drawCmd instanceof TextSpriteDrawCmd) { return postScript.toString().getBytes(); } + private static double roundCoordinate(double value) { + return Math.round(value * 10000.0) / 10000.0; + } + /** * Utility to create postscript draw operations from a shapes path * iterator. diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/commands/PushDrawCmd.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/commands/PushDrawCmd.java new file mode 100644 index 000000000..12e65a904 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/commands/PushDrawCmd.java @@ -0,0 +1,7 @@ +package org.icepdf.core.pobjects.graphics.commands; + +/** + * document this class, still needed? + */ +public class PushDrawCmd extends PostScriptDrawCmd { +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/FaxDecoder.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/FaxDecoder.java index 2a4571675..593fd5f58 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/FaxDecoder.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/FaxDecoder.java @@ -37,7 +37,7 @@ public class FaxDecoder extends AbstractImageDecoder { private static final Logger logger = - Logger.getLogger(JBig2Decoder.class.toString()); + Logger.getLogger(FaxDecoder.class.toString()); public static final Name K_KEY = new Name("K"); public static final Name ENCODED_BYTE_ALIGN_KEY = new Name("EncodedByteAlign"); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/ImageStream.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/ImageStream.java index 5e3c616ac..59ba0c35c 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/ImageStream.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/ImageStream.java @@ -82,6 +82,10 @@ public BufferedImage getImage(GraphicsState graphicsState, Resources resources){ return tmp; } } + // corner case when working with newly added images + if (decodedImage != null) { + return decodedImage; + } // decode the given image. ImageDecoder imageDecoder = ImageDecoderFactory.createDecoder(this, graphicsState); BufferedImage decodedImage = imageDecoder.decode(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RasterDecoder.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RasterDecoder.java index da1b8493e..58d69839e 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RasterDecoder.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RasterDecoder.java @@ -24,7 +24,7 @@ public class RasterDecoder extends AbstractImageDecoder { private static final Logger logger = - Logger.getLogger(JpxDecoder.class.toString()); + Logger.getLogger(RasterDecoder.class.toString()); public RasterDecoder(ImageStream imageStream, GraphicsState graphicsState) { super(imageStream, graphicsState); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RawDecoder.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RawDecoder.java index da1dda295..dd19998d4 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RawDecoder.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/RawDecoder.java @@ -28,7 +28,7 @@ public class RawDecoder extends AbstractImageDecoder { private static final Logger logger = - Logger.getLogger(JpxDecoder.class.toString()); + Logger.getLogger(RawDecoder.class.toString()); public RawDecoder(ImageStream imageStream, GraphicsState graphicsState) { super(imageStream, graphicsState); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/BlurredImageReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/BlurredImageReference.java index 411710460..8f81977b6 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/BlurredImageReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/BlurredImageReference.java @@ -1,5 +1,6 @@ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -59,9 +60,9 @@ public class BlurredImageReference extends CachedImageReference { } } - protected BlurredImageReference(ImageStream imageStream, GraphicsState graphicsState, + protected BlurredImageReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page page) { - super(imageStream, graphicsState, resources, imageIndex, page); + super(imageStream, xobjectName, graphicsState, resources, imageIndex, page); // kick off a new thread to load the image, if not already in pool. ImagePool imagePool = imageStream.getLibrary().getImagePool(); if (useProxy && imagePool.get(reference) == null) { @@ -92,7 +93,7 @@ public BufferedImage call() { logger.finest("Falling back on smooth scaled image reference processing. "); } image = new SmoothScaledImageReference( - imageStream, graphicsState, resources, imageIndex, parentPage).call(); + imageStream, xobjectName, graphicsState, resources, imageIndex, parentPage).call(); } } catch (Exception e) { logger.log(Level.WARNING, e, () -> "Error loading image: " + imageStream.getPObjectReference() + diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/CachedImageReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/CachedImageReference.java index cb5b73e90..9c18ce883 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/CachedImageReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/CachedImageReference.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -35,10 +36,10 @@ public abstract class CachedImageReference extends ImageReference { private final ImagePool imagePool; private boolean isNull; - protected CachedImageReference(ImageStream imageStream, GraphicsState graphicsState, + protected CachedImageReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page page) { - super(imageStream, graphicsState, resources, imageIndex, page); + super(imageStream, xobjectName, graphicsState, resources, imageIndex, page); imagePool = imageStream.getLibrary().getImagePool(); this.reference = imageStream.getPObjectReference(); } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageContentWriterReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageContentWriterReference.java new file mode 100644 index 000000000..454a41ae1 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageContentWriterReference.java @@ -0,0 +1,40 @@ +package org.icepdf.core.pobjects.graphics.images.references; + +import org.icepdf.core.pobjects.Name; +import org.icepdf.core.pobjects.graphics.images.ImageStream; + +import java.awt.image.BufferedImage; + +/** + * ImageContentWriterReference isn't actually used for rendering and is instead used a state placeholder for + * ImageStreams so the ImageDrawCmd can be converted to postscript by the PostScriptEncoder. The ImageStream + * object contains a Buffered image which will be encoded and inserted into a content stream. + */ +public class ImageContentWriterReference extends ImageReference { + + + public ImageContentWriterReference(ImageStream imageStream, Name xobjectName) { + super(imageStream, xobjectName, null, null, 0, null); + image = imageStream.getDecodedImage(); + } + + @Override + public int getWidth() { + return image.getWidth(); + } + + @Override + public int getHeight() { + return image.getHeight(); + } + + @Override + public BufferedImage getImage() throws InterruptedException { + return image; + } + + @Override + public BufferedImage call() throws Exception { + return image; + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReference.java index db50df70f..97d693f7d 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReference.java @@ -18,6 +18,7 @@ import org.icepdf.core.events.PageImageEvent; import org.icepdf.core.events.PageLoadingEvent; import org.icepdf.core.events.PageLoadingListener; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Reference; import org.icepdf.core.pobjects.Resources; @@ -60,13 +61,15 @@ public abstract class ImageReference implements Callable { protected Resources resources; protected BufferedImage image; protected Reference reference; + protected Name xobjectName; protected int imageIndex; protected Page parentPage; - protected ImageReference(ImageStream imageStream, GraphicsState graphicsState, + protected ImageReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page parentPage) { this.imageStream = imageStream; + this.xobjectName = xobjectName; this.graphicsState = graphicsState; this.resources = resources; this.imageIndex = imageIndex; @@ -149,6 +152,10 @@ public boolean isImage() { return image != null; } + public Name getXobjectName() { + return xobjectName; + } + protected void notifyPageImageLoadedEvent(long duration, boolean interrupted) { if (parentPage != null) { PageImageEvent pageLoadingEvent = diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReferenceFactory.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReferenceFactory.java index fa7ff3c2b..03e252bb4 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReferenceFactory.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageReferenceFactory.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -99,25 +100,29 @@ public static ImageReference getImageReferenceType(String imageReferenceType) { * or by the static instance variable scale type. * * @param imageStream image data + * @param xobjectName image name if specified via do xobject reference, can be null * @param resources parent resource object. * @param graphicsState image graphic state. - * @param page page that image belongs to . - * @param imageIndex image index number of total images for the page. + * @param page page that image belongs to . + * @param imageIndex image index number of total images for the page. * @return newly create ImageReference. */ public static org.icepdf.core.pobjects.graphics.images.references.ImageReference getImageReference( - ImageStream imageStream, Resources resources, GraphicsState graphicsState, Integer imageIndex, Page page) { + ImageStream imageStream, Name xobjectName, Resources resources, GraphicsState graphicsState, + Integer imageIndex, Page page) { switch (imageReferenceType) { case SCALED: - return new ScaledImageReference(imageStream, graphicsState, resources, imageIndex, page); + return new ScaledImageReference(imageStream, xobjectName, graphicsState, resources, imageIndex, page); case SMOOTH_SCALED: - return new SmoothScaledImageReference(imageStream, graphicsState, resources, imageIndex, page); + return new SmoothScaledImageReference(imageStream, xobjectName, graphicsState, resources, imageIndex, + page); case MIP_MAP: - return new MipMappedImageReference(imageStream, graphicsState, resources, imageIndex, page); + return new MipMappedImageReference(imageStream, xobjectName, graphicsState, resources, imageIndex, + page); case BLURRED: - return new BlurredImageReference(imageStream, graphicsState, resources, imageIndex, page); + return new BlurredImageReference(imageStream, xobjectName, graphicsState, resources, imageIndex, page); default: - return new ImageStreamReference(imageStream, graphicsState, resources, imageIndex, page); + return new ImageStreamReference(imageStream, xobjectName, graphicsState, resources, imageIndex, page); } } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageStreamReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageStreamReference.java index ef630bbc4..a6db447aa 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageStreamReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ImageStreamReference.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -28,11 +29,11 @@ /** * The ImageStreamReference class is a rudimentary Image Proxy which will - * try and decode the image data into an Buffered image using a worker thread. + * try and decode the image data into a Buffered image using a worker thread. * The intent is that the content parser will continue parsing the content stream - * while the worker thread handles the image decode work. However the drawImage + * while the worker thread handles the image decode work. However, the drawImage * method will block until the worker thread returns. So generally put not - * a true image proxy but we do get significantly faster load times with the + * a true image proxy, but we do get significantly faster load times with the * current implementation. * * @since 5.0 @@ -42,10 +43,10 @@ public class ImageStreamReference extends CachedImageReference { private static final Logger logger = Logger.getLogger(ImageStreamReference.class.toString()); - protected ImageStreamReference(ImageStream imageStream, GraphicsState graphicsState, + protected ImageStreamReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page page) { - super(imageStream, graphicsState, resources, imageIndex, page); + super(imageStream, xobjectName, graphicsState, resources, imageIndex, page); // kick off a new thread to load the image, if not already in pool. ImagePool imagePool = imageStream.getLibrary().getImagePool(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/InlineImageStreamReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/InlineImageStreamReference.java index 26af28793..26430d730 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/InlineImageStreamReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/InlineImageStreamReference.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -37,10 +38,10 @@ public class InlineImageStreamReference extends ImageReference { private static final Logger logger = Logger.getLogger(InlineImageStreamReference.class.toString()); - public InlineImageStreamReference(ImageStream imageStream, GraphicsState graphicsState, + public InlineImageStreamReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int iamgeIndex, Page page) { - super(imageStream, graphicsState, resources, iamgeIndex, page); + super(imageStream, xobjectName, graphicsState, resources, iamgeIndex, page); // kick off a new thread to load the image, if not already in pool. ImagePool imagePool = imageStream.getLibrary().getImagePool(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/MipMappedImageReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/MipMappedImageReference.java index 52b3ea401..0afef6360 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/MipMappedImageReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/MipMappedImageReference.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -43,16 +44,16 @@ class MipMappedImageReference extends ImageReference { private final ArrayList images; - protected MipMappedImageReference(ImageStream imageStream, GraphicsState graphicsState, + protected MipMappedImageReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page page) { - super(imageStream, graphicsState, resources, imageIndex, page); + super(imageStream, xobjectName, graphicsState, resources, imageIndex, page); images = new ArrayList<>(); ImageReference imageReference = - new ImageStreamReference(imageStream, graphicsState, resources, imageIndex, page); + new ImageStreamReference(imageStream, xobjectName, graphicsState, resources, imageIndex, page); images.add(imageReference); int width = imageReference.getWidth(); @@ -64,7 +65,7 @@ protected MipMappedImageReference(ImageStream imageStream, GraphicsState graphic while (width > 20 && height > 20) { width /= 2; height /= 2; - imageReference = new ScaledImageReference(imageReference, graphicsState, resources, + imageReference = new ScaledImageReference(imageReference, xobjectName, graphicsState, resources, width, height, imageIndex, page); images.add(imageReference); } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ScaledImageReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ScaledImageReference.java index ae5831aa0..ddd49c766 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ScaledImageReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/ScaledImageReference.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.GraphicsState; @@ -43,9 +44,9 @@ public class ScaledImageReference extends CachedImageReference { private final int width; private final int height; - protected ScaledImageReference(ImageStream imageStream, GraphicsState graphicsState, + protected ScaledImageReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page page) { - super(imageStream, graphicsState, resources, imageIndex, page); + super(imageStream, xobjectName, graphicsState, resources, imageIndex, page); // get eh original image width. width = imageStream.getWidth(); @@ -61,14 +62,15 @@ protected ScaledImageReference(ImageStream imageStream, GraphicsState graphicsSt } } - public ScaledImageReference(ImageReference imageReference, GraphicsState graphicsState, Resources resources, + public ScaledImageReference(ImageReference imageReference, Name xobjectName, GraphicsState graphicsState, + Resources resources, int width, int height, int imageIndex, Page page) throws InterruptedException { - super(imageReference.getImageStream(), graphicsState, resources, imageIndex, page); + super(imageReference.getImageStream(), xobjectName, graphicsState, resources, imageIndex, page); this.width = width; this.height = height; - // check for an repeated scale via a call from MipMap + // check for a repeated scale via a call from MipMap if (imageReference.isImage()) { image = imageReference.getImage(); } diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/SmoothScaledImageReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/SmoothScaledImageReference.java index ff270660f..800d2aae7 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/SmoothScaledImageReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/graphics/images/references/SmoothScaledImageReference.java @@ -15,6 +15,7 @@ */ package org.icepdf.core.pobjects.graphics.images.references; +import org.icepdf.core.pobjects.Name; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.graphics.DeviceGray; @@ -65,10 +66,10 @@ public class SmoothScaledImageReference extends CachedImageReference { private final int width; private final int height; - protected SmoothScaledImageReference(ImageStream imageStream, GraphicsState graphicsState, + protected SmoothScaledImageReference(ImageStream imageStream, Name xobjectName, GraphicsState graphicsState, Resources resources, int imageIndex, Page page) { - super(imageStream, graphicsState, resources, imageIndex, page); + super(imageStream, xobjectName, graphicsState, resources, imageIndex, page); // get eh original image width. width = imageStream.getWidth(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/LoadJceProvider.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/LoadJceProvider.java new file mode 100644 index 000000000..afdefa67b --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/LoadJceProvider.java @@ -0,0 +1,48 @@ +package org.icepdf.core.pobjects.security; + +import org.icepdf.core.util.Defs; + +import java.lang.reflect.InvocationTargetException; +import java.security.Provider; +import java.security.Security; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Load Bouncy Castle JCE provider with the option to use a customer provider using the system property + * -Dorg.icepdf.core.security.jceProvider=myProviderOfChoice. Bouncy Castle is required for loading + * some encrypted documents as well as adding digital signatures. + */ +public class LoadJceProvider { + + private static final Logger logger = + Logger.getLogger(LoadJceProvider.class.toString()); + + /** + * Try and load a JCE provider specified by org.icepdf.core.security.jceProvider system property. If not set + * fall back on using BouncyCastleProvider. + */ + public static void loadProvider() { + // Load security handler from system property if possible + String defaultSecurityProvider = "org.bouncycastle.jce.provider.BouncyCastleProvider"; + + // check system property security provider + String customSecurityProvider = Defs.sysProperty("org.icepdf.core.security.jceProvider"); + + // if no custom security provider load default security provider + if (customSecurityProvider != null) { + defaultSecurityProvider = customSecurityProvider; + } + try { + // try and create a new provider + Object provider = Class.forName(defaultSecurityProvider).getDeclaredConstructor().newInstance(); + Security.addProvider((Provider) provider); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Optional BouncyCastle security provider not found"); + } catch (InstantiationException e) { + logger.log(Level.FINE, "Optional BouncyCastle security provider could not be instantiated"); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + logger.log(Level.FINE, "Optional BouncyCastle security provider could not be created"); + } + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/SecurityManager.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/SecurityManager.java index e5b08e544..711b0382b 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/SecurityManager.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/security/SecurityManager.java @@ -62,51 +62,10 @@ public class SecurityManager { // Pointer to class which implements the SecurityHandler interface private final SecurityHandler securityHandler; - // flag for detecting JCE - private static boolean foundJCE = false; - // key caches, fairly expensive calculation private byte[] encryptionKey; private byte[] decryptionKey; - // Add security provider of choice before Sun RSA provider (if any) - static { - // Load security handler from system property if possible - String defaultSecurityProvider = - "org.bouncycastle.jce.provider.BouncyCastleProvider"; - - // check system property security provider - String customSecurityProvider = - Defs.sysProperty("org.icepdf.core.security.jceProvider"); - - // if no custom security provider load default security provider - if (customSecurityProvider != null) { - defaultSecurityProvider = customSecurityProvider; - } - try { - // try and create a new provider - Object provider = Class.forName(defaultSecurityProvider).getDeclaredConstructor().newInstance(); - Security.addProvider((Provider) provider); - } catch (ClassNotFoundException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider not found"); - } catch (NoSuchMethodException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider no such method error"); - } catch (InstantiationException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider could not be instantiated"); - } catch (IllegalAccessException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider could not be created"); - } catch (InvocationTargetException e) { - logger.log(Level.FINE, "Optional BouncyCastle security provider invocation target exception"); - } - - try { - Class.forName("javax.crypto.Cipher"); - foundJCE = true; - } catch (ClassNotFoundException e) { - logger.log(Level.SEVERE, "Sun JCE Support Not Found"); - } - } - /** * Disposes of the security handler instance. */ @@ -126,13 +85,6 @@ public SecurityManager(Library library, DictionaryEntries encryptionDictionary, List fileID) throws PDFSecurityException { - // Check to make sure that if run under JDK 1.3 that the JCE libraries - // are installed as extra packages - if (!foundJCE) { - logger.log(Level.SEVERE, "Sun JCE support was not found on classpath"); - throw new PDFSecurityException("Sun JCE Support Not Found"); - } - // create dictionary for document encryptDictionary = new EncryptionDictionary(library, encryptionDictionary, fileID); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReference.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReference.java index f16f074fc..df23c1537 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReference.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReference.java @@ -16,6 +16,9 @@ public interface CrossReference { PObject loadObject(ObjectLoader objectLoader, Reference reference, Name hint) throws ObjectStateException, CrossReferenceStateException, IOException; + int getObjectOffset(ObjectLoader objectLoader, Reference reference) + throws ObjectStateException, CrossReferenceStateException, IOException; + CrossReferenceEntry getEntry(Reference reference) throws ObjectStateException, CrossReferenceStateException, IOException; diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceBase.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceBase.java index ea60b2d59..2aca476e9 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceBase.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceBase.java @@ -38,6 +38,14 @@ public PObject loadObject(ObjectLoader objectLoader, Reference reference, Name h return null; } + public int getObjectOffset(ObjectLoader objectLoader, Reference reference) + throws ObjectStateException, CrossReferenceStateException, IOException { + if (reference != null) { + return objectLoader.getObjectOffset(this, reference); + } + return -1; + } + public CrossReferenceEntry getEntry(Reference reference) throws ObjectStateException, CrossReferenceStateException, IOException { CrossReferenceEntry crossReferenceEntry = indirectObjectReferences.get(reference); diff --git a/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceRoot.java b/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceRoot.java index a1a9de0a2..d6c35879c 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceRoot.java +++ b/core/core-awt/src/main/java/org/icepdf/core/pobjects/structure/CrossReferenceRoot.java @@ -110,6 +110,14 @@ public PObject loadObject(ObjectLoader objectLoader, Reference reference, Name h return null; } + public int getObjectOffset(ObjectLoader objectLoader, Reference reference, Name hint) + throws ObjectStateException, CrossReferenceStateException, IOException { + for (CrossReference crossReference : crossReferences) { + return crossReference.getObjectOffset(objectLoader, reference); + } + return -1; + } + public void setInitializationFailed(boolean failed) { initializationFailed = failed; } diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/Library.java b/core/core-awt/src/main/java/org/icepdf/core/util/Library.java index e395831cf..933765dfd 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/Library.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/Library.java @@ -27,6 +27,7 @@ import org.icepdf.core.pobjects.graphics.ICCBased; import org.icepdf.core.pobjects.graphics.images.ImageStream; import org.icepdf.core.pobjects.graphics.images.references.ImagePool; +import org.icepdf.core.pobjects.security.LoadJceProvider; import org.icepdf.core.pobjects.security.SecurityManager; import org.icepdf.core.pobjects.structure.CrossReferenceRoot; import org.icepdf.core.pobjects.structure.Header; @@ -88,6 +89,7 @@ public class Library { } catch (NumberFormatException e) { logger.warning("Error reading buffered scale factor"); } + LoadJceProvider.loadProvider(); } private final ConcurrentHashMap> objectStore = @@ -111,6 +113,7 @@ public class Library { // handles signature validation and signing. private final SignatureHandler signatureHandler; + private final SignatureManager signatureManager; // signature permissions private Permissions permissions; @@ -123,7 +126,6 @@ public class Library { private boolean isLinearTraversal; private final ImagePool imagePool; - /** * Creates a new instance of a Library. */ @@ -132,6 +134,7 @@ public Library() { // set Catalog memory Manager and cache manager. imagePool = new ImagePool(); signatureHandler = new SignatureHandler(); + signatureManager = new SignatureManager(); } /** @@ -518,6 +521,10 @@ public boolean isValidEntry(DictionaryEntries dictionaryEntries, Name key) { return o != null && (!(o instanceof Reference) || isValidEntry((Reference) o)); } + public int getOffset(Reference reference) throws CrossReferenceStateException, ObjectStateException, IOException { + return crossReferenceRoot.getObjectOffset(objectLoader, reference, null); + } + /** * Tests if there exists a cross-reference entry for this reference. * @@ -898,6 +905,10 @@ public SignatureHandler getSignatureHandler() { return signatureHandler; } + public SignatureManager getSignatureDictionaries() { + return signatureManager; + } + /** * Set a documents permissions for a given certificate of signature, optional. * The permission should also be used with the encryption permissions if present diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/PropertyConstants.java b/core/core-awt/src/main/java/org/icepdf/core/util/PropertyConstants.java index c7925e87f..487f86e51 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/PropertyConstants.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/PropertyConstants.java @@ -73,5 +73,4 @@ public class PropertyConstants { ANNOTATION_SUMMARY_BOX_FONT_SIZE_CHANGE = "annotationSummaryBoxFontSizeChange"; - } diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/SignatureManager.java b/core/core-awt/src/main/java/org/icepdf/core/util/SignatureManager.java new file mode 100644 index 000000000..91457ba46 --- /dev/null +++ b/core/core-awt/src/main/java/org/icepdf/core/util/SignatureManager.java @@ -0,0 +1,98 @@ +package org.icepdf.core.util; + +import org.icepdf.core.pobjects.PObject; +import org.icepdf.core.pobjects.StateManager; +import org.icepdf.core.pobjects.acroform.InteractiveForm; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; +import org.icepdf.core.pobjects.acroform.SignatureReferenceDictionary; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; + +import java.util.ArrayList; +import java.util.List; + +/** + * SignatureManager is used to manage the signature dictionaries associated with a document. A users can create + * more than one Signature annotation but, they must be linked to the same SignatureDictionary. When a document is + * written to disk, only one signature dictionary can be used to sign the document. + *

+ * This class also does basic validation to make sure there is only one dictionary marked as the + * /DocMDP or "certifier" distinction. + */ +public class SignatureManager { + + private SignatureDictionary currentSignatureDictionary; + private final ArrayList signatureWidgetAnnotations = new ArrayList<>(); + + public void addSignature(SignatureDictionary signatureDictionary, SignatureWidgetAnnotation signatureAnnotation) { + // if not the same dictionary then we need to apply it to all the existing signature widgets and clean up + // the old dictionary. + if (currentSignatureDictionary != null && !currentSignatureDictionary.equals(signatureDictionary)) { + for (SignatureWidgetAnnotation signatureWidgetAnnotation : signatureWidgetAnnotations) { + signatureWidgetAnnotation.setSignatureDictionary(signatureDictionary); + } + // remove the old signature dictionary + StateManager stateManager = signatureAnnotation.getLibrary().getStateManager(); + stateManager.removeChange(new PObject(currentSignatureDictionary, + currentSignatureDictionary.getPObjectReference())); + } + + currentSignatureDictionary = signatureDictionary; + signatureAnnotation.setSignatureDictionary(currentSignatureDictionary); + + // add the new signature widget to the list + if (!signatureWidgetAnnotations.contains(signatureAnnotation)) { + signatureWidgetAnnotations.add(signatureAnnotation); + } + } + + /** + * Clears the current signature dictionary and references to associated SignatureWidgetAnnotation. + * This should be done after the document has been signed or if the signature process is cancelled. + */ + public void clearSignatures() { + currentSignatureDictionary = null; + signatureWidgetAnnotations.clear(); + } + + /** + * Returns the signature dictionaries associated with the document edits and will be used to sign the document. + * + * @return current signature dictionary for signing or null if not set. + */ + public SignatureDictionary getCurrentSignatureDictionary() { + return currentSignatureDictionary; + } + + /** + * Check if a signature dictionary has been set. If a signature dictionary has been set, then the current + * signature dictionary should be used to sign the document and a new one should not be created + * + * @return true if a signature dictionary has been set, otherwise false. + */ + public boolean hasSignatureDictionary() { + return currentSignatureDictionary != null; + } + + /** + * Checks to see if a certifier signature already exists in the document. + * + * @param library document library + * @return true if there is already a certifier signature, otherwise false. + */ + public boolean hasExistingCertifier(Library library) { + InteractiveForm interactiveForm = library.getCatalog().getInteractiveForm(); + if (interactiveForm != null) { + ArrayList signatureWidgets = interactiveForm.getSignatureFields(); + for (SignatureWidgetAnnotation signatureWidget : signatureWidgets) { + List signatureReferenceDictionary = + signatureWidget.getSignatureDictionary().getReferences(); + for (SignatureReferenceDictionary reference : signatureReferenceDictionary) { + if (reference.getTransformMethod() == SignatureReferenceDictionary.TransformMethods.DocMDP) { + return true; + } + } + } + } + return false; + } +} diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/AbstractContentParser.java b/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/AbstractContentParser.java index e9b27b7a8..8c00e560b 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/AbstractContentParser.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/AbstractContentParser.java @@ -648,7 +648,7 @@ else if (viewParse) { // create an ImageReference for future decoding ImageReference imageReference = ImageReferenceFactory.getImageReference( - imageStream, resources, graphicState, + imageStream, xobjectName, resources, graphicState, imageIndex.get(), page); imageIndex.incrementAndGet(); diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/ContentParser.java b/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/ContentParser.java index 82b5cc9b5..5167aaf2c 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/ContentParser.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/parser/content/ContentParser.java @@ -1052,14 +1052,14 @@ private void parseInlineImage(Lexer lexer, Shapes shapes, Page page) throws IOEx // create the image stream imageStream = new ImageStream(library, iih, data); imageStreamReference = ImageReferenceFactory.getImageReference( - imageStream, resources, graphicState, imageIndex.get(), page); + imageStream, null, resources, graphicState, imageIndex.get(), page); inlineImageCache.put(tmpKey, imageStreamReference); } } else { // create the image stream imageStream = new ImageStream(library, iih, data); imageStreamReference = ImageReferenceFactory.getImageReference( - imageStream, resources, graphicState, imageIndex.get(), page); + imageStream, null, resources, graphicState, imageIndex.get(), page); } // experimental display // ImageUtility.displayImage(imageStreamReference.getImage(), "BI"); diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/parser/object/ObjectLoader.java b/core/core-awt/src/main/java/org/icepdf/core/util/parser/object/ObjectLoader.java index c3f52bbe7..410509a2c 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/parser/object/ObjectLoader.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/parser/object/ObjectLoader.java @@ -47,4 +47,20 @@ public synchronized PObject loadObject(CrossReference crossReference, Reference } return null; } + + public synchronized int getObjectOffset(CrossReference crossReference, Reference reference) + throws ObjectStateException, CrossReferenceStateException, IOException { + + CrossReferenceEntry entry = crossReference.getEntry(reference); + + if (entry instanceof CrossReferenceUsedEntry) { + CrossReferenceUsedEntry crossReferenceEntry = (CrossReferenceUsedEntry) entry; + // parse the object + int offset = crossReferenceEntry.getFilePositionOfObject(); + return offset; + } else if (entry instanceof CrossReferenceCompressedEntry) { + throw new IllegalStateException("The cross reference compressed entry is not supported."); + } + return -1; + } } \ No newline at end of file diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/updater/DocumentBuilder.java b/core/core-awt/src/main/java/org/icepdf/core/util/updater/DocumentBuilder.java index 073731c9a..d3cd301a1 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/updater/DocumentBuilder.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/updater/DocumentBuilder.java @@ -5,9 +5,6 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; -import java.util.logging.Level; import java.util.logging.Logger; /** @@ -28,29 +25,21 @@ public long createDocument( OutputStream out, long documentLength) throws IOException, InterruptedException { - try (WritableByteChannel channel = Channels.newChannel(out)) { - if (writeMode == WriteMode.FULL_UPDATE) { - // kick of a full rewrite of the document, replacing any updates objects with new data - long newLength = new FullUpdater().writeDocument( - document, - out); - return newLength; - } else if (writeMode == WriteMode.INCREMENT_UPDATE) { - // copy original file data - channel.write(documentByteBuffer); - // append the data from the incremental updater - long appendedLength = new IncrementalUpdater().appendIncrementalUpdate( - document, - out, - documentLength); - channel.close(); - return documentLength + appendedLength; - } - } catch (IOException | InterruptedException e) { - logger.log(Level.FINE, "Error writing PDF output stream.", e); - throw e; + long length = -1; + if (writeMode == WriteMode.FULL_UPDATE) { + // kick of a full rewrite of the document, replacing any updates objects with new data + length = new FullUpdater().writeDocument( + document, + out); + } else if (writeMode == WriteMode.INCREMENT_UPDATE) { + // append the data from the incremental updater + long appendedLength = new IncrementalUpdater().appendIncrementalUpdate( + document, + documentByteBuffer, + out, + documentLength); + length = documentLength + appendedLength; } - - return 0; + return length; } } diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/updater/FullUpdater.java b/core/core-awt/src/main/java/org/icepdf/core/util/updater/FullUpdater.java index b1c264e7f..176bcc10f 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/updater/FullUpdater.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/updater/FullUpdater.java @@ -3,20 +3,25 @@ import org.icepdf.core.exceptions.PDFSecurityException; import org.icepdf.core.io.CountingOutputStream; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.acroform.signature.DocumentSigner; import org.icepdf.core.pobjects.graphics.images.references.ImageReference; import org.icepdf.core.pobjects.security.SecurityManager; import org.icepdf.core.pobjects.structure.CrossReferenceRoot; import org.icepdf.core.util.Defs; import org.icepdf.core.util.Library; +import org.icepdf.core.util.SignatureManager; import org.icepdf.core.util.redaction.Redactor; import org.icepdf.core.util.updater.writeables.BaseWriter; +import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Writes a document stream in its entirety. The document's root object is used ot traverse the page tree @@ -27,6 +32,9 @@ */ public class FullUpdater { + private static final Logger logger = + Logger.getLogger(FullUpdater.class.toString()); + /** * Write the xrefTable in a compressed format by default. Can be disabled if to aid in debugging or to * support old PDF versions. @@ -49,9 +57,9 @@ public static void setCompressXrefTable(boolean compressXrefTable) { * Write a new document inserting and updating modified objects to the specified output stream. * * @param document The Document that is being saved - * @param outputStream OutputStream to write the incremental update to + * @param outputStream OutputStream to write the full document to * @return The number of bytes written generating the new document - * @throws java.io.IOException error writing stream. + * @throws java.io.IOException error writing stream. * @throws InterruptedException */ public long writeDocument( @@ -59,23 +67,53 @@ public long writeDocument( throws IOException, InterruptedException { // create a tmp file and write the changed document - Path tmpFile = Files.createTempFile(null, null); - OutputStream tmpOutputStream = new FileOutputStream(tmpFile.toFile()); - writeDocument(document, tmpOutputStream, false); + Path tmpFilePath = Files.createTempFile(null, null); + Path tmpRedactionFilePath = Files.createTempFile(null, null); + OutputStream tmpOutputStream = new FileOutputStream(tmpFilePath.toFile()); + OutputStream tmpRedactionOutputStream = null; + long bytesWritten = writeDocument(document, tmpOutputStream, false); tmpOutputStream.close(); // open the copy and burn the redactions to the specified outputStream + if (stateManager.hasRedactions()) { + Document tmpDocument = new Document(); + tmpRedactionOutputStream = new FileOutputStream(tmpRedactionFilePath.toFile()); + try { + tmpDocument.setFile(tmpFilePath.toString()); + bytesWritten = writeDocument(tmpDocument, tmpRedactionOutputStream, true); + } catch (PDFSecurityException e) { + throw new RuntimeException(e); + } finally { + // clean up + tmpDocument.dispose(); + tmpRedactionOutputStream.close(); + } + } + Path currentPath; + if (tmpRedactionOutputStream != null) { + currentPath = tmpRedactionFilePath; + } else { + currentPath = tmpFilePath; + } + // apply any signatures Document tmpDocument = new Document(); - long bytesWritten; try { - tmpDocument.setFile(tmpFile.toString()); - bytesWritten = writeDocument(tmpDocument, outputStream, true); - } catch (PDFSecurityException e) { + SignatureManager signatureManager = library.getSignatureDictionaries(); + if (signatureManager.hasSignatureDictionary()) { + tmpDocument.setFile(currentPath.toString()); + File tempFile = currentPath.toFile(); + DocumentSigner.signDocument(tmpDocument, tempFile, + signatureManager.getCurrentSignatureDictionary()); + } + Files.copy(currentPath, outputStream); + } catch (Exception e) { + logger.log(Level.FINE, "Failed to sign document.", e); throw new RuntimeException(e); } finally { - // clean up + // clean of the tmp files tmpDocument.dispose(); - Files.delete(tmpFile); + Files.delete(tmpFilePath); + Files.delete(tmpRedactionFilePath); } return bytesWritten; } diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/updater/IncrementalUpdater.java b/core/core-awt/src/main/java/org/icepdf/core/util/updater/IncrementalUpdater.java index 891c5b711..baf7ffa0f 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/updater/IncrementalUpdater.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/updater/IncrementalUpdater.java @@ -5,37 +5,68 @@ import org.icepdf.core.pobjects.PObject; import org.icepdf.core.pobjects.PTrailer; import org.icepdf.core.pobjects.StateManager; +import org.icepdf.core.pobjects.acroform.signature.DocumentSigner; import org.icepdf.core.pobjects.security.SecurityManager; import org.icepdf.core.pobjects.structure.CrossReferenceRoot; +import org.icepdf.core.util.Library; +import org.icepdf.core.util.SignatureManager; import org.icepdf.core.util.updater.writeables.BaseWriter; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; public class IncrementalUpdater { + private static final Logger logger = + Logger.getLogger(IncrementalUpdater.class.toString()); + /** * Appends modified objects to the specified output stream. * * @param document The Document that is being saved + * @param documentByteBuffer ByteBuffer of the original document * @param outputStream OutputStream to write the incremental update to * @param documentLength start of appender bytes, can be zero if storing the bytes to another source. * @return The number of bytes written in the incremental update * @throws java.io.IOException error writing stream. */ public long appendIncrementalUpdate( - Document document, OutputStream outputStream, long documentLength) + Document document, ByteBuffer documentByteBuffer, OutputStream outputStream, long documentLength) throws IOException { + Library library = document.getCatalog().getLibrary(); + SignatureManager signatureManager = library.getSignatureDictionaries(); StateManager stateManager = document.getStateManager(); CrossReferenceRoot crossReferenceRoot = stateManager.getCrossReferenceRoot(); - if (stateManager.isNoChange()) { + if (stateManager.isNoChange() && !signatureManager.hasSignatureDictionary()) { return 0L; } + // create a temp document so that it can sign it after the incremental update + Path tmpFilePath = Files.createTempFile(null, null); + File tempFile = tmpFilePath.toFile(); + OutputStream newDocumentOutputStream = new FileOutputStream(tempFile); + try { + // copy original file data + WritableByteChannel channel = Channels.newChannel(newDocumentOutputStream); + channel.write(documentByteBuffer); + } catch (IOException e) { + logger.log(Level.FINE, "Error writing PDF output stream during incremental write.", e); + throw e; + } + SecurityManager securityManager = document.getSecurityManager(); - CountingOutputStream output = new CountingOutputStream(outputStream); + CountingOutputStream output = new CountingOutputStream(newDocumentOutputStream); BaseWriter writer = new BaseWriter(crossReferenceRoot, securityManager, output, documentLength); writer.initializeWriters(); @@ -49,7 +80,7 @@ public long appendIncrementalUpdate( } } - // todo may need updating as I don't think it handles hybrid mode + // todo, may need updating as I don't think it handles hybrid mode properly PTrailer trailer = crossReferenceRoot.getTrailerDictionary(); if (trailer.isCompressedXref()) { writer.writeIncrementalCompressedXrefTable(); @@ -59,6 +90,29 @@ public long appendIncrementalUpdate( } output.close(); + // sign the document using the first signature, this could be reworked to handle more signatures, like + // certification followed by other approvals. But for now it will be assumed this is done as seperate steps + Document tmpDocument = new Document(); + try { + if (signatureManager.hasSignatureDictionary()) { + // open new incrementally updated tmp file + tmpDocument.setFile(tempFile.toString()); + // size of new file, this won't change as SignatureDictionary has padding to account for content and + // offsets + DocumentSigner.signDocument(tmpDocument, tempFile, + signatureManager.getCurrentSignatureDictionary()); + } + } catch (Exception e) { + logger.log(Level.FINE, "Failed to sign document.", e); + throw new RuntimeException(e); + } finally { + tmpDocument.dispose(); + } + + // copy the temp file to the outputStream and cleanup. + Files.copy(tmpFilePath, outputStream); + Files.delete(tmpFilePath); + return writer.getBytesWritten(); } } diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/updater/modifiers/AnnotationRemovalModifier.java b/core/core-awt/src/main/java/org/icepdf/core/util/updater/modifiers/AnnotationRemovalModifier.java index d97174e5b..d3ba178c7 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/updater/modifiers/AnnotationRemovalModifier.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/updater/modifiers/AnnotationRemovalModifier.java @@ -1,6 +1,7 @@ package org.icepdf.core.util.updater.modifiers; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; import org.icepdf.core.pobjects.annotations.*; import org.icepdf.core.util.Library; @@ -37,7 +38,7 @@ public void modify(Annotation annot) { Stream nAp = annot.getAppearanceStream(); if (nAp != null) { nAp.setDeleted(true); - // find the xObjects font resources. + // clean up resources. Object tmp = library.getObject(nAp.getEntries(), RESOURCES_KEY); if (tmp instanceof Resources) { Resources resources = (Resources) tmp; @@ -48,7 +49,26 @@ public void modify(Annotation annot) { font.setDeleted(true); stateManager.addDeletion(font.getPObjectReference()); } + DictionaryEntries xObject = resources.getXObjects(); + if (xObject != null) { + for (Object key : xObject.keySet()) { + Object obj = xObject.get(key); + if (obj instanceof Reference) { + stateManager.addDeletion((Reference) obj); + } + } + } + } + } + // check for /V key which is a reference to a signature dictionary + // todo new annotation base method to encapsulate the cleanup + if (annot instanceof SignatureWidgetAnnotation) { + SignatureWidgetAnnotation signatureWidgetAnnotation = (SignatureWidgetAnnotation) annot; + Object v = signatureWidgetAnnotation.getEntries().get(SignatureDictionary.V_KEY); + if (v instanceof Reference) { + stateManager.addDeletion((Reference) v); } + library.getSignatureDictionaries().clearSignatures(); } // check to see if this is an existing annotations, if the annotations diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/BaseWriter.java b/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/BaseWriter.java index 17881dee6..136102251 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/BaseWriter.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/BaseWriter.java @@ -63,8 +63,7 @@ public BaseWriter() { } public BaseWriter(CrossReferenceRoot crossReferenceRoot, SecurityManager securityManager, - CountingOutputStream output, - long startingPosition) { + CountingOutputStream output, long startingPosition) { this.output = output; this.crossReferenceRoot = crossReferenceRoot; this.securityManager = securityManager; @@ -153,7 +152,7 @@ public void writeHeader(Header header) throws IOException { headerWriter.write(header, output); } - protected void writeValue(PObject pObject, CountingOutputStream output) throws IOException { + public void writeValue(PObject pObject, CountingOutputStream output) throws IOException { Object val = pObject.getObject(); if (val == null) { output.write(NULL); @@ -255,16 +254,12 @@ protected byte[] encryptStream(Stream stream, byte[] outputData) throws IOExcept Library library = stream.getLibrary(); if (stream.getEntries().get(Stream.DECODEPARAM_KEY) != null) { // needed to check for a custom crypt filter - decodeParams = library.getDictionary(stream.getEntries(), - Stream.DECODEPARAM_KEY); + decodeParams = library.getDictionary(stream.getEntries(), Stream.DECODEPARAM_KEY); } else { decodeParams = new DictionaryEntries(); } - InputStream decryptedStream = securityManager.encryptInputStream( - stream.getPObjectReference(), - securityManager.getDecryptionKey(), - decodeParams, - new ByteArrayInputStream(outputData), true); + InputStream decryptedStream = securityManager.encryptInputStream(stream.getPObjectReference(), + securityManager.getDecryptionKey(), decodeParams, new ByteArrayInputStream(outputData), true); ByteArrayOutputStream out = new ByteArrayOutputStream(); int nRead; byte[] data = new byte[16384]; diff --git a/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/image/PredictorEncoder.java b/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/image/PredictorEncoder.java index ffa1564cb..f69bf7067 100644 --- a/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/image/PredictorEncoder.java +++ b/core/core-awt/src/main/java/org/icepdf/core/util/updater/writeables/image/PredictorEncoder.java @@ -77,6 +77,7 @@ class PredictorEncoder implements ImageEncoder { PredictorEncoder(ImageStream imageStream) { this.imageStream = imageStream; BufferedImage image = imageStream.getDecodedImage(); + // The raw count of components per pixel including optional alpha this.componentsPerPixel = image.getColorModel().getNumComponents(); int transferType = image.getRaster().getTransferType(); @@ -123,7 +124,7 @@ class PredictorEncoder implements ImageEncoder { /** * Tries to compress the image using a predictor. * - * @return the image or null if it is not possible to encoded the image (e.g. not supported + * @return the image or null if it is not possible to encode the image (e.g. not supported * raster format etc.) */ public ImageStream encode() throws IOException { diff --git a/examples/signatures/src/main/java/org/icepdf/examples/signatures/Pkcs11SignatureCreation.java b/examples/signatures/src/main/java/org/icepdf/examples/signatures/Pkcs11SignatureCreation.java new file mode 100644 index 000000000..180492c90 --- /dev/null +++ b/examples/signatures/src/main/java/org/icepdf/examples/signatures/Pkcs11SignatureCreation.java @@ -0,0 +1,180 @@ +package org.icepdf.examples.signatures; + +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDate; +import org.icepdf.core.pobjects.acroform.FieldDictionaryFactory; +import org.icepdf.core.pobjects.acroform.InteractiveForm; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; +import org.icepdf.core.pobjects.acroform.signature.SignatureValidator; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureType; +import org.icepdf.core.pobjects.acroform.signature.handlers.Pkcs11SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.handlers.SimplePasswordCallbackHandler; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; +import org.icepdf.core.pobjects.annotations.AnnotationFactory; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.core.util.Library; +import org.icepdf.core.util.SignatureManager; +import org.icepdf.core.util.updater.WriteMode; +import org.icepdf.ri.common.views.annotations.signing.BasicSignatureAppearanceCallback; +import org.icepdf.ri.common.views.annotations.signing.SignatureAppearanceModelImpl; +import org.icepdf.ri.util.FontPropertiesManager; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.nio.file.Path; +import java.util.Locale; + +/** + * The Pkcs11SignatureCreation class is an example of how to sign a document with a digital signatures + * using PKCS#11 provider. More information on the pkcs11 configuration file can be found here, + * https://docs.oracle.com/en/java/javase/11/security/pkcs11-reference-guide1.html + * + * @since 6.3 + */ +public class Pkcs11SignatureCreation { + + static { + // read/store the font cache. + FontPropertiesManager.getInstance().loadOrReadSystemFonts(); + } + + public static void main(String[] args) { + // Get a file from the command line to open + String filePath = args[0]; + String providerConfig = args[1]; + BigInteger certSerial = convertHexStringToBigInteger(args[2]); + String password = args[3]; + Path path = Path.of(filePath); + // start the capture + new Pkcs11SignatureCreation().signDocument(path, providerConfig, certSerial, password); + } + + private static BigInteger convertHexStringToBigInteger(String hexStr) { + hexStr = hexStr.replace(":", ""); + return new BigInteger(hexStr, 16); + } + + public void signDocument(Path filePath, String providerConfig, BigInteger certSerial, String password) { + try { + + Pkcs11SignerHandler pkcs11SignerHandler = new Pkcs11SignerHandler( + providerConfig, + certSerial, + new SimplePasswordCallbackHandler(password)); + + Document document = new Document(); + document.setFile(filePath.toFile().getPath()); + Library library = document.getCatalog().getLibrary(); + SignatureManager signatureManager = library.getSignatureDictionaries(); + + // Creat signature annotation + SignatureWidgetAnnotation signatureAnnotation = (SignatureWidgetAnnotation) + AnnotationFactory.buildWidgetAnnotation( + document.getPageTree().getLibrary(), + FieldDictionaryFactory.TYPE_SIGNATURE, + new Rectangle(100, 250, 300, 150)); + document.getPageTree().getPage(0).addAnnotation(signatureAnnotation, true); + + // Add the signatureWidget to catalog + InteractiveForm interactiveForm = document.getCatalog().getOrCreateInteractiveForm(); + interactiveForm.addField(signatureAnnotation); + + // update dictionary + SignatureDictionary signatureDictionary = SignatureDictionary.getInstance(signatureAnnotation, + SignatureType.SIGNER); + signatureDictionary.setSignerHandler(pkcs11SignerHandler); + signatureDictionary.setName("Tester McTest"); + signatureDictionary.setLocation("Springfield"); + signatureDictionary.setReason("Make sure stuff didn't change"); + signatureDictionary.setDate("D:20240423082733+02'00'"); + signatureManager.addSignature(signatureDictionary, signatureAnnotation); + + // assign cert metadata to dictionary + SignatureUtilities.updateSignatureDictionary(signatureDictionary, pkcs11SignerHandler.getCertificate()); + + // build basic appearance + SignatureAppearanceModelImpl signatureAppearanceModel = new SignatureAppearanceModelImpl(library); + signatureAppearanceModel.setLocale(Locale.ENGLISH); + signatureAppearanceModel.setName(signatureDictionary.getName()); + signatureAppearanceModel.setContact(signatureDictionary.getContactInfo()); + signatureAppearanceModel.setLocation(signatureDictionary.getLocation()); + signatureAppearanceModel.setSignatureType(signatureDictionary.getReason().equals("Approval") ? + SignatureType.SIGNER : SignatureType.CERTIFIER); + signatureAppearanceModel.setSignatureImageVisible(false); + + BasicSignatureAppearanceCallback signatureAppearance = new BasicSignatureAppearanceCallback(); + signatureAppearance.setSignatureAppearanceModel(signatureAppearanceModel); + signatureAnnotation.setAppearanceCallback(signatureAppearance); + signatureAnnotation.resetAppearanceStream(new AffineTransform()); + + String absolutePath = filePath.toFile().getPath(); + String signedFileName = absolutePath.replace(".pdf", "_signed.pdf"); + + File out = new File(signedFileName); + try (BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(out), 8192)) { + document.saveToOutputStream(stream, WriteMode.INCREMENT_UPDATE); + } + + // open the signed document + Document modifiedDocument = new Document(); + modifiedDocument.setFile(out.getAbsolutePath()); + + } catch (Exception e) { + // make sure we have no io errors. + e.printStackTrace(); + } + } + + private static void printSignatureSummary(SignatureWidgetAnnotation signatureWidgetAnnotation) { + SignatureDictionary signatureDictionary = signatureWidgetAnnotation.getSignatureDictionary(); + String signingTime = new PDate(signatureWidgetAnnotation.getLibrary().getSecurityManager(), + signatureDictionary.getDate()).toString(); + System.out.println("General Info:"); + System.out.println(" Signing time: " + signingTime); + System.out.println(" Reason: " + signatureDictionary.getReason()); + System.out.println(" Location: " + signatureDictionary.getLocation()); + } + + /** + * Print out some summary data of the validator results. + * + * @param signatureValidator validator to show properties data. + */ + private static void printValidationSummary(SignatureValidator signatureValidator) { + System.out.println("Singer Info:"); + if (signatureValidator.isCertificateChainTrusted()) { + System.out.println(" Path validation checks were successful"); + } else { + System.out.println(" Path validation checks were unsuccessful"); + } + if (!signatureValidator.isCertificateChainTrusted() || signatureValidator.isRevocation()) { + System.out.println(" Revocation checking was not performed"); + } else { + System.out.println(" Signer's certificate is valid and has not been revoked"); + } + System.out.println("Validity Summary:"); + if (!signatureValidator.isSignedDataModified() && !signatureValidator.isDocumentDataModified()) { + System.out.println(" Document has not been modified since it was signed"); + } else if (!signatureValidator.isSignedDataModified() && signatureValidator.isDocumentDataModified() && signatureValidator.isSignaturesCoverDocumentLength()) { + System.out.println(" This version of the document is unaltered but subsequent changes have been made"); + } else if (!signatureValidator.isSignaturesCoverDocumentLength()) { + System.out.println(" Document has been altered or corrupted sing it was singed"); + } + if (!signatureValidator.isCertificateDateValid()) { + System.out.println(" Signers certificate has expired"); + } + if (signatureValidator.isEmbeddedTimeStamp()) { + System.out.println(" Signature included an embedded timestamp but it could not be validated"); + } else { + System.out.println(" Signing time is from the clock on this signer's computer"); + } + if (signatureValidator.isSelfSigned()) { + System.out.println(" Document is self signed"); + } + System.out.println(); + } +} diff --git a/examples/signatures/src/main/java/org/icepdf/examples/signatures/Pkcs12SignatureCreation.java b/examples/signatures/src/main/java/org/icepdf/examples/signatures/Pkcs12SignatureCreation.java new file mode 100644 index 000000000..03b870320 --- /dev/null +++ b/examples/signatures/src/main/java/org/icepdf/examples/signatures/Pkcs12SignatureCreation.java @@ -0,0 +1,172 @@ +package org.icepdf.examples.signatures; + +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDate; +import org.icepdf.core.pobjects.acroform.FieldDictionaryFactory; +import org.icepdf.core.pobjects.acroform.InteractiveForm; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; +import org.icepdf.core.pobjects.acroform.signature.SignatureValidator; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureType; +import org.icepdf.core.pobjects.acroform.signature.handlers.Pkcs12SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.handlers.SimplePasswordCallbackHandler; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; +import org.icepdf.core.pobjects.annotations.AnnotationFactory; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.core.util.Library; +import org.icepdf.core.util.SignatureManager; +import org.icepdf.core.util.updater.WriteMode; +import org.icepdf.ri.common.views.annotations.signing.BasicSignatureAppearanceCallback; +import org.icepdf.ri.common.views.annotations.signing.SignatureAppearanceModelImpl; +import org.icepdf.ri.util.FontPropertiesManager; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.util.Locale; + +/** + * The Pkcs12SignatureCreation class is an example of how to sign a document with a digital signatures + * using PKCS#12 provider. + * + * @since 6.3 + */ +public class Pkcs12SignatureCreation { + + static { + // read/store the font cache. + FontPropertiesManager.getInstance().loadOrReadSystemFonts(); + } + + public static void main(String[] args) { + // Get a file from the command line to open + String filePath = args[0]; + // path to keystore file, keystore-keypair.pfx + String keyStorePath = args[1]; + // cert alias + String alias = args[2]; + // keystore password + String password = args[3]; + Path path = Path.of(filePath); + // start the capture + new Pkcs12SignatureCreation().signDocument(path, keyStorePath, alias, password); + } + + public void signDocument(Path filePath, String keyStorePath, String certAlias, String password) { + try { + + Pkcs12SignerHandler pkcs12SignerHandler = new Pkcs12SignerHandler( + new File(keyStorePath), + certAlias, + new SimplePasswordCallbackHandler(password)); + + Document document = new Document(); + document.setFile(filePath.toFile().getPath()); + Library library = document.getCatalog().getLibrary(); + SignatureManager signatureManager = library.getSignatureDictionaries(); + + // Creat signature annotation + SignatureWidgetAnnotation signatureAnnotation = (SignatureWidgetAnnotation) + AnnotationFactory.buildWidgetAnnotation( + document.getPageTree().getLibrary(), + FieldDictionaryFactory.TYPE_SIGNATURE, + new Rectangle(100, 250, 100, 50)); + document.getPageTree().getPage(0).addAnnotation(signatureAnnotation, true); + + // Add the signatureWidget to catalog + InteractiveForm interactiveForm = document.getCatalog().getOrCreateInteractiveForm(); + interactiveForm.addField(signatureAnnotation); + + // update dictionary + SignatureDictionary signatureDictionary = SignatureDictionary.getInstance(signatureAnnotation, + SignatureType.SIGNER); + signatureDictionary.setSignerHandler(pkcs12SignerHandler); + signatureDictionary.setName("Tester McTest"); + signatureDictionary.setLocation("Springfield"); + signatureDictionary.setReason("Make sure stuff didn't change"); + signatureDictionary.setDate("D:20240423082733+02'00'"); + signatureManager.addSignature(signatureDictionary, signatureAnnotation); + + // assign cert metadata to dictionary + SignatureUtilities.updateSignatureDictionary(signatureDictionary, pkcs12SignerHandler.getCertificate()); + + // build basic appearance + SignatureAppearanceModelImpl signatureAppearanceModel = new SignatureAppearanceModelImpl(library); + signatureAppearanceModel.setLocale(Locale.ENGLISH); + signatureAppearanceModel.setName(signatureDictionary.getName()); + signatureAppearanceModel.setContact(signatureDictionary.getContactInfo()); + signatureAppearanceModel.setLocation(signatureDictionary.getLocation()); + signatureAppearanceModel.setSignatureType(signatureDictionary.getReason().equals("Approval") ? + SignatureType.SIGNER : SignatureType.CERTIFIER); + signatureAppearanceModel.setSignatureImageVisible(false); + + BasicSignatureAppearanceCallback signatureAppearance = new BasicSignatureAppearanceCallback(); + signatureAppearance.setSignatureAppearanceModel(signatureAppearanceModel); + signatureAnnotation.setAppearanceCallback(signatureAppearance); + signatureAnnotation.resetAppearanceStream(new AffineTransform()); + + String absolutePath = filePath.toFile().getPath(); + String signedFileName = absolutePath.replace(".pdf", "_signed.pdf"); + + File out = new File(signedFileName); + try (BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(out), 8192)) { + document.saveToOutputStream(stream, WriteMode.INCREMENT_UPDATE); + } + document.dispose(); + } catch (Exception e) { + // make sure we have no io errors. + e.printStackTrace(); + } + } + + private static void printSignatureSummary(SignatureWidgetAnnotation signatureWidgetAnnotation) { + SignatureDictionary signatureDictionary = signatureWidgetAnnotation.getSignatureDictionary(); + String signingTime = new PDate(signatureWidgetAnnotation.getLibrary().getSecurityManager(), + signatureDictionary.getDate()).toString(); + System.out.println("General Info:"); + System.out.println(" Signing time: " + signingTime); + System.out.println(" Reason: " + signatureDictionary.getReason()); + System.out.println(" Location: " + signatureDictionary.getLocation()); + } + + /** + * Print out some summary data of the validator results. + * + * @param signatureValidator validator to show properties data. + */ + private static void printValidationSummary(SignatureValidator signatureValidator) { + System.out.println("Singer Info:"); + if (signatureValidator.isCertificateChainTrusted()) { + System.out.println(" Path validation checks were successful"); + } else { + System.out.println(" Path validation checks were unsuccessful"); + } + if (!signatureValidator.isCertificateChainTrusted() || signatureValidator.isRevocation()) { + System.out.println(" Revocation checking was not performed"); + } else { + System.out.println(" Signer's certificate is valid and has not been revoked"); + } + System.out.println("Validity Summary:"); + if (!signatureValidator.isSignedDataModified() && !signatureValidator.isDocumentDataModified()) { + System.out.println(" Document has not been modified since it was signed"); + } else if (!signatureValidator.isSignedDataModified() && signatureValidator.isDocumentDataModified() && signatureValidator.isSignaturesCoverDocumentLength()) { + System.out.println(" This version of the document is unaltered but subsequent changes have been made"); + } else if (!signatureValidator.isSignaturesCoverDocumentLength()) { + System.out.println(" Document has been altered or corrupted sing it was singed"); + } + if (!signatureValidator.isCertificateDateValid()) { + System.out.println(" Signers certificate has expired"); + } + if (signatureValidator.isEmbeddedTimeStamp()) { + System.out.println(" Signature included an embedded timestamp but it could not be validated"); + } else { + System.out.println(" Signing time is from the clock on this signer's computer"); + } + if (signatureValidator.isSelfSigned()) { + System.out.println(" Document is self signed"); + } + System.out.println(); + } +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/MyAnnotationCallback.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/MyAnnotationCallback.java index 3d2742d1a..88440b75f 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/MyAnnotationCallback.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/MyAnnotationCallback.java @@ -15,24 +15,27 @@ */ package org.icepdf.ri.common; -import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.Name; +import org.icepdf.core.pobjects.Page; +import org.icepdf.core.pobjects.PageTree; +import org.icepdf.core.pobjects.acroform.InteractiveForm; import org.icepdf.core.pobjects.actions.*; +import org.icepdf.core.pobjects.annotations.AbstractWidgetAnnotation; import org.icepdf.core.pobjects.annotations.Annotation; import org.icepdf.core.pobjects.annotations.LinkAnnotation; import org.icepdf.core.pobjects.annotations.MarkupAnnotation; import org.icepdf.ri.common.views.*; -import org.icepdf.ri.common.views.annotations.AbstractAnnotationComponent; import org.icepdf.ri.common.views.annotations.MarkupAnnotationComponent; -import org.icepdf.ri.common.views.annotations.PopupAnnotationComponent; import org.icepdf.ri.util.BareBonesBrowserLaunch; import java.io.File; -import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; /** * This class represents a basic implementation of the AnnotationCallback + * * @since 2.6 */ public class MyAnnotationCallback implements AnnotationCallback { @@ -233,6 +236,12 @@ public void removeAnnotation(PageViewComponent pageComponent, } } } + // corner case for acroform widget annotations, todo creat a formal api for clean up. + if (annotationComponent.getAnnotation() instanceof AbstractWidgetAnnotation) { + InteractiveForm interactiveForm = + documentViewController.getDocument().getCatalog().getOrCreateInteractiveForm(); + interactiveForm.removeField((AbstractWidgetAnnotation) annotationComponent.getAnnotation()); + } } } diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingController.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingController.java index ff6d09970..1167f7a72 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingController.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingController.java @@ -224,6 +224,8 @@ public class SwingController extends ComponentAdapter implements org.icepdf.ri.c private JButton deleteAllAnnotationsButton; private AnnotationColorToggleButton highlightAnnotationToolButton; private JToggleButton redactionAnnotationToolButton; + + private JToggleButton signatureAnnotationToolButton; private JToggleButton linkAnnotationToolButton; private AnnotationColorToggleButton strikeOutAnnotationToolButton; private AnnotationColorToggleButton underlineAnnotationToolButton; @@ -1284,6 +1286,11 @@ public void setRedactionAnnotationToolButton(JToggleButton btn) { btn.addItemListener(this); } + public void setSignatureAnnotationToolButton(JToggleButton btn) { + signatureAnnotationToolButton = btn; + btn.addItemListener(this); + } + /** * Called by SwingViewerBuilder, so that Controller can setup event handling * @@ -1729,6 +1736,7 @@ protected void reflectStateInComponents() { setEnabled(deleteAllAnnotationsButton, opened && canModify && !pdfCollection && !IS_READONLY); setEnabled(highlightAnnotationToolButton, opened && canModify && !pdfCollection && !IS_READONLY); setEnabled(redactionAnnotationToolButton, opened && canModify && !pdfCollection && !IS_READONLY); + setEnabled(signatureAnnotationToolButton, opened && canModify && !pdfCollection && !IS_READONLY); setEnabled(strikeOutAnnotationToolButton, opened && canModify && !pdfCollection && !IS_READONLY); setEnabled(underlineAnnotationToolButton, opened && canModify && !pdfCollection && !IS_READONLY); setEnabled(lineAnnotationToolButton, opened && canModify && !pdfCollection && !IS_READONLY); @@ -1754,7 +1762,7 @@ protected void reflectStateInComponents() { setEnabled(freeTextAnnotationPropertiesToolButton, opened && canModify && !pdfCollection && !IS_READONLY); setEnabled(annotationPrivacyComboBox, opened && !pdfCollection && !IS_READONLY); setEnabled(textAnnotationPropertiesToolButton, opened && canModify && !pdfCollection && !IS_READONLY); - setEnabled(formHighlightButton, opened && !pdfCollection && hasForms()); + setEnabled(formHighlightButton, opened && !pdfCollection); setEnabled(quickSearchToolBar, opened && !pdfCollection); setEnabled(facingPageViewContinuousButton, opened && !pdfCollection); setEnabled(singlePageViewContinuousButton, opened && !pdfCollection); @@ -2006,6 +2014,11 @@ public void setDisplayTool(final int argToolName) { documentViewController.setToolMode(DocumentViewModelImpl.DISPLAY_TOOL_LINK_ANNOTATION); documentViewController.setViewCursor(DocumentViewController.CURSOR_CROSSHAIR); setCursorOnComponents(DocumentViewController.CURSOR_DEFAULT); + } else if (argToolName == DocumentViewModelImpl.DISPLAY_TOOL_SIGNATURE_ANNOTATION) { + actualToolMayHaveChanged = + documentViewController.setToolMode(DocumentViewModelImpl.DISPLAY_TOOL_SIGNATURE_ANNOTATION); + documentViewController.setViewCursor(DocumentViewController.CURSOR_CROSSHAIR); + setCursorOnComponents(DocumentViewController.CURSOR_DEFAULT); } else if (argToolName == DocumentViewModelImpl.DISPLAY_TOOL_REDACTION_ANNOTATION) { actualToolMayHaveChanged = documentViewController.setToolMode(DocumentViewModelImpl.DISPLAY_TOOL_REDACTION_ANNOTATION); @@ -2109,7 +2122,7 @@ private void setCursorOnComponents(final int cursorType) { } /** - * Sets the state of the "Tools" buttons. This ensure that correct button + * Sets the state of the "Tools" buttons. This ensures that correct button * is depressed when the state of the Document class specifies it. */ private void reflectToolInToolButtons() { @@ -2123,6 +2136,8 @@ private void reflectToolInToolButtons() { documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_HIGHLIGHT_ANNOTATION)); reflectSelectionInButton(redactionAnnotationToolButton, documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_REDACTION_ANNOTATION)); + reflectSelectionInButton(signatureAnnotationToolButton, + documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_SIGNATURE_ANNOTATION)); reflectSelectionInButton(underlineAnnotationToolButton, documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_UNDERLINE_ANNOTATION)); reflectSelectionInButton(strikeOutAnnotationToolButton, @@ -2131,6 +2146,8 @@ private void reflectToolInToolButtons() { documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_LINE_ANNOTATION)); reflectSelectionInButton(linkAnnotationToolButton, documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_LINK_ANNOTATION)); + reflectSelectionInButton(signatureAnnotationToolButton, + documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_SIGNATURE_ANNOTATION)); reflectSelectionInButton(lineArrowAnnotationToolButton, documentViewController.isToolModeSelected(DocumentViewModelImpl.DISPLAY_TOOL_LINE_ARROW_ANNOTATION)); reflectSelectionInButton(squareAnnotationToolButton, @@ -3237,6 +3254,7 @@ public void dispose() { selectToolButton = null; highlightAnnotationToolButton = null; redactionAnnotationToolButton = null; + signatureAnnotationToolButton = null; strikeOutAnnotationToolButton = null; underlineAnnotationToolButton = null; lineAnnotationToolButton = null; @@ -3516,25 +3534,7 @@ protected void saveFileChecks(SaveMode saveMode, String originalFileName, File f // but that could cause problems with slow network links too, // and would complicate the incremental update code, so we're // harmonising on this approach. - try (final FileOutputStream fileOutputStream = new FileOutputStream(file); - final BufferedOutputStream buf = new BufferedOutputStream(fileOutputStream, 8192)) { - - // We want 'save as' or 'save a copy to always occur - if (saveMode == SaveMode.EXPORT) { - // save as copy - document.writeToOutputStream(buf, WriteMode.FULL_UPDATE); - } else { - // save as will append changes. - document.saveToOutputStream(buf); - } - document.getStateManager().setChangesSnapshot(); - } catch (MalformedURLException e) { - logger.log(Level.WARNING, "Malformed URL Exception ", e); - } catch (IOException e) { - logger.log(Level.WARNING, "IO Exception ", e); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to append document changes", e); - } + writeDocument(saveMode, file); // save the default directory ViewModel.setDefaultFile(file); } @@ -3551,6 +3551,26 @@ protected void saveFileChecks(SaveMode saveMode, String originalFileName, File f } } + private void writeDocument(SaveMode saveMode, File file) { + try (final FileOutputStream fileOutputStream = new FileOutputStream(file); + final BufferedOutputStream buf = new BufferedOutputStream(fileOutputStream, 8192)) { + if (saveMode == SaveMode.EXPORT) { + // save as copy + document.writeToOutputStream(buf, WriteMode.FULL_UPDATE); + } else { + // save as will append changes. + document.writeToOutputStream(buf, WriteMode.INCREMENT_UPDATE); + } + document.getStateManager().setChangesSnapshot(); + } catch (MalformedURLException e) { + logger.log(Level.WARNING, "Malformed URL Exception ", e); + } catch (IOException e) { + logger.log(Level.WARNING, "IO Exception ", e); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to append document changes", e); + } + } + /** * Generates a file name based on the original file name but appends "-new". * If new file extension exists a ".pdf" is automatically added. @@ -5040,6 +5060,11 @@ else if (source == selectToolButton) { tool = DocumentViewModelImpl.DISPLAY_TOOL_REDACTION_ANNOTATION; setDocumentToolMode(DocumentViewModelImpl.DISPLAY_TOOL_REDACTION_ANNOTATION); } + } else if (source == signatureAnnotationToolButton) { + if (e.getStateChange() == ItemEvent.SELECTED) { + tool = DocumentViewModelImpl.DISPLAY_TOOL_SIGNATURE_ANNOTATION; + setDocumentToolMode(DocumentViewModelImpl.DISPLAY_TOOL_SIGNATURE_ANNOTATION); + } } else if (checkAnnotationButton(source, strikeOutAnnotationToolButton, strikeOutAnnotationPropertiesToolButton)) { if (e.getStateChange() == ItemEvent.SELECTED) { diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingViewBuilder.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingViewBuilder.java index a42fade0c..20866618e 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingViewBuilder.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/SwingViewBuilder.java @@ -1667,6 +1667,10 @@ public JToolBar buildAnnotationToolBar() { ViewerPropertiesManager.PROPERTY_SHOW_TOOLBAR_ANNOTATION_REDACTION)) { addToToolBar(toolbar, buildRedactionAnnotationToolButton(iconSize)); } + if (propertiesManager.checkAndStoreBooleanProperty( + ViewerPropertiesManager.PROPERTY_SHOW_TOOLBAR_ANNOTATION_SIGNATURE)) { + addToToolBar(toolbar, buildSignatureAnnotationToolButton(iconSize)); + } if (SystemProperties.PRIVATE_PROPERTY_ENABLED && propertiesManager.checkAndStoreBooleanProperty( ViewerPropertiesManager.PROPERTY_SHOW_TOOLBAR_ANNOTATION_PERMISSION)) { addToToolBar(toolbar, buildAnnotationPermissionCombBox()); @@ -1830,6 +1834,16 @@ public JToggleButton buildRedactionAnnotationToolButton(final Images.IconSize im return btn; } + public JToggleButton buildSignatureAnnotationToolButton(final Images.IconSize imageSize) { + JToggleButton btn = makeToolbarToggleButton( + messageBundle.getString("viewer.toolbar.tool.signature.label"), + messageBundle.getString("viewer.toolbar.tool.signature.tooltip"), + "signature_annot", imageSize, buttonFont); + if (viewerController != null && btn != null) + viewerController.setSignatureAnnotationToolButton(btn); + return btn; + } + public AbstractButton buildStrikeOutAnnotationToolButton(final Images.IconSize imageSize) { AnnotationColorToggleButton btn = makeAnnotationToggleButton( messageBundle.getString("viewer.toolbar.tool.strikeOut.label"), diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/PreferencesDialog.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/PreferencesDialog.java index 06a6a31e0..45c74f48f 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/PreferencesDialog.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/PreferencesDialog.java @@ -68,7 +68,7 @@ public PreferencesDialog(Frame frame, SwingController controller, constraints = new GridBagConstraints(); constraints.fill = GridBagConstraints.BOTH; constraints.weightx = 1.0; - constraints.weighty = 0; + constraints.weighty = 1.0; constraints.insets = new Insets(5, 5, 5, 5); constraints.anchor = GridBagConstraints.NORTH; addGB(layoutPanel, propertiesTabbedPane, 0, 0, 1, 1); @@ -101,6 +101,13 @@ protected ImagingPreferencesPanel createImagingPreferencesPanel(SwingController } + protected SigningPreferencesPanel createSigningPreferencesPanel(SwingController controller, + ViewerPropertiesManager propertiesManager, + ResourceBundle messageBundle) { + return new SigningPreferencesPanel(controller, propertiesManager, messageBundle); + + } + protected FontsPreferencesPanel createFontsPreferencesPanel(SwingController controller, ViewerPropertiesManager propertiesManager, ResourceBundle messageBundle) { @@ -147,6 +154,13 @@ protected JTabbedPane createTabbedPane(SwingController controller, ResourceBundl messageBundle.getString("viewer.dialog.viewerPreferences.section.imaging.title"), createImagingPreferencesPanel(controller, propertiesManager, messageBundle)); } + // build the signing preferences tab + if (propertiesManager.checkAndStoreBooleanProperty( + ViewerPropertiesManager.PROPERTY_SHOW_PREFERENCES_SIGNING)) { + propertiesTabbedPane.addTab( + messageBundle.getString("viewer.dialog.viewerPreferences.section.signatures.title"), + createSigningPreferencesPanel(controller, propertiesManager, messageBundle)); + } // build the fonts preferences tab if (propertiesManager.checkAndStoreBooleanProperty( ViewerPropertiesManager.PROPERTY_SHOW_PREFERENCES_FONTS)) { diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/SigningPreferencesPanel.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/SigningPreferencesPanel.java new file mode 100644 index 000000000..1c617fdcb --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/preferences/SigningPreferencesPanel.java @@ -0,0 +1,201 @@ +package org.icepdf.ri.common.preferences; + +import org.icepdf.ri.common.SwingController; +import org.icepdf.ri.util.ViewerPropertiesManager; + +import javax.swing.*; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; +import java.awt.*; +import java.util.ResourceBundle; +import java.util.prefs.Preferences; + +/** + * Contains singing setting for the viewer reference implementation. Allows users to pick the default signing handler. + *

    + *
  • Pkcs11SigningHandlerEnables smart card Pkcs11 keystore capabilities
  • + *
  • Pkcs12SigningHandlerEnables standard Pkcs12 keystore capabilities
  • + *
+ * + * @since 7.3 + */ +public class SigningPreferencesPanel extends JPanel { + + public static final String PKCS_11_TYPE = "PKCS#11"; + public static final String PKCS_12_TYPE = "PKCS#12"; + + private static final short PKCS11 = 0; + private static final short PKCS12 = 1; + + // layouts constraint + private final GridBagConstraints constraints; + private final JComboBox keystoreTypeComboBox; + private final JLabel pkcsPathLabel; + private final JTextField pkcsPathTextField; + + private final ResourceBundle messageBundle; + private final Preferences preferences; + + public SigningPreferencesPanel(SwingController controller, ViewerPropertiesManager propertiesManager, + ResourceBundle messageBundle) { + + super(new GridBagLayout()); + setAlignmentY(JPanel.TOP_ALIGNMENT); + + preferences = propertiesManager.getPreferences(); + this.messageBundle = messageBundle; + + KeystoreTypeItem[] pkcsTypeItems = + new KeystoreTypeItem[]{ + new KeystoreTypeItem(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.11.label"), + PKCS_11_TYPE), + new KeystoreTypeItem(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.12.label"), + PKCS_12_TYPE), + }; + + keystoreTypeComboBox = new JComboBox<>(pkcsTypeItems); + keystoreTypeComboBox.setSelectedItem(new KeystoreTypeItem("", + preferences.get(ViewerPropertiesManager.PROPERTY_PKCS_KEYSTORE_TYPE, pkcsTypeItems[PKCS12].value))); + + // setup default state + pkcsPathLabel = new JLabel(); + pkcsPathTextField = new JTextField(); + updatePkcsPaths(); + JButton pkcsPathBrowseButton = new JButton(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.keystore.path.browse.label")); + pkcsPathBrowseButton.addActionListener(e -> showBrowseDialog()); + + pkcsPathTextField.addActionListener(e -> savePkcsPaths(keystoreTypeComboBox)); + + keystoreTypeComboBox.addActionListener(e -> { + updatePkcsPaths(); + }); + + JPanel imagingPreferencesPanel = new JPanel(new GridBagLayout()); + imagingPreferencesPanel.setAlignmentY(JPanel.TOP_ALIGNMENT); + imagingPreferencesPanel.setBorder(new TitledBorder(new EtchedBorder(EtchedBorder.LOWERED), + messageBundle.getString("viewer.dialog.viewerPreferences.section.signatures.pkcs.border.label"), + TitledBorder.LEFT, + TitledBorder.DEFAULT_POSITION)); + + constraints = new GridBagConstraints(); + constraints.fill = GridBagConstraints.NONE; + constraints.weightx = 1; + constraints.weighty = 0; + constraints.anchor = GridBagConstraints.WEST; + constraints.insets = new Insets(5, 5, 5, 5); + + addGB(imagingPreferencesPanel, new JLabel(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.label")), + 0, 0, 1, 1); + + constraints.anchor = GridBagConstraints.EAST; + addGB(imagingPreferencesPanel, keystoreTypeComboBox, 1, 0, 2, 1); + + constraints.anchor = GridBagConstraints.WEST; + addGB(imagingPreferencesPanel, pkcsPathLabel, 0, 1, 1, 1); + constraints.anchor = GridBagConstraints.EAST; + constraints.weightx = 1.0; + constraints.fill = GridBagConstraints.BOTH; + addGB(imagingPreferencesPanel, pkcsPathTextField, 1, 1, 1, 1); + addGB(imagingPreferencesPanel, pkcsPathBrowseButton, 2, 1, 1, 1); + + constraints.anchor = GridBagConstraints.NORTHWEST; + constraints.fill = GridBagConstraints.BOTH; + addGB(this, imagingPreferencesPanel, 0, 0, 1, 1); + // little spacer + constraints.weighty = 1.0; + addGB(this, new Label(" "), 0, 1, 1, 1); + } + + private void updatePkcsPaths() { + // update which config/keystore input to show. + if (keystoreTypeComboBox.getSelectedIndex() == PKCS11) { + pkcsPathLabel.setText(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.11.config.path.label")); + pkcsPathTextField.setText(preferences.get(ViewerPropertiesManager.PROPERTY_PKCS11_PROVIDER_CONFIG_PATH, + "")); + } else if (keystoreTypeComboBox.getSelectedIndex() == PKCS12) { + pkcsPathLabel.setText(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.12.keystore.path.label")); + pkcsPathTextField.setText(preferences.get(ViewerPropertiesManager.PROPERTY_PKCS12_PROVIDER_KEYSTORE_PATH, + "")); + } + KeystoreTypeItem selectedItem = (KeystoreTypeItem) keystoreTypeComboBox.getSelectedItem(); + if (selectedItem != null) { + preferences.put(ViewerPropertiesManager.PROPERTY_PKCS_KEYSTORE_TYPE, selectedItem.getValue()); + } + } + + private void savePkcsPaths(JComboBox cb) { + // update which config/keystore input to show. + if (cb.getSelectedIndex() == PKCS11) { + preferences.put(ViewerPropertiesManager.PROPERTY_PKCS11_PROVIDER_CONFIG_PATH, pkcsPathTextField.getText()); + } else if (cb.getSelectedIndex() == PKCS12) { + preferences.put(ViewerPropertiesManager.PROPERTY_PKCS12_PROVIDER_KEYSTORE_PATH, + pkcsPathTextField.getText()); + } + } + + private void showBrowseDialog() { + String pkcsPath = pkcsPathTextField.getText(); + JFileChooser fileChooser = new JFileChooser(pkcsPath); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setDialogTitle(messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.keystore.path.selection.title")); + final int responseValue = fileChooser.showDialog(this, messageBundle.getString( + "viewer.dialog.viewerPreferences.section.signatures.pkcs.keystore.path.accept.label")); + if (responseValue == JFileChooser.APPROVE_OPTION) { + pkcsPathTextField.setText(fileChooser.getSelectedFile().getAbsolutePath()); + savePkcsPaths(keystoreTypeComboBox); + updatePkcsPaths(); + } + + } + + private void addGB(JPanel layout, Component component, + int x, int y, + int rowSpan, int colSpan) { + constraints.gridx = x; + constraints.gridy = y; + constraints.gridwidth = rowSpan; + constraints.gridheight = colSpan; + layout.add(component, constraints); + } + + static class KeystoreTypeItem { + final String label; + final String value; + + public KeystoreTypeItem(String label, String value) { + this.label = label; + this.value = value; + } + + public String getLabel() { + return label; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return label; + } + + @Override + public boolean equals(Object keystoreTypeItem) { + if (keystoreTypeItem instanceof KeystoreTypeItem) { + return value.equals(((KeystoreTypeItem) keystoreTypeItem).getValue()); + } else { + return value.equals(keystoreTypeItem); + } + } + } + +} \ No newline at end of file diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/tools/SignatureAnnotationHandler.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/tools/SignatureAnnotationHandler.java new file mode 100644 index 000000000..0a9d801c2 --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/tools/SignatureAnnotationHandler.java @@ -0,0 +1,132 @@ +package org.icepdf.ri.common.tools; + +import org.icepdf.core.pobjects.acroform.FieldDictionaryFactory; +import org.icepdf.core.pobjects.acroform.InteractiveForm; +import org.icepdf.core.pobjects.annotations.AnnotationFactory; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.ri.common.ViewModel; +import org.icepdf.ri.common.views.AbstractPageViewComponent; +import org.icepdf.ri.common.views.DocumentViewController; +import org.icepdf.ri.common.views.annotations.AbstractAnnotationComponent; +import org.icepdf.ri.common.views.annotations.AnnotationComponentFactory; +import org.icepdf.ri.util.ViewerPropertiesManager; + +import javax.swing.event.MouseInputListener; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.util.logging.Logger; + +/** + * Creates a placeholder for a digital signature. Placeholder can be used to add signatures fields to a document, + * signers can then add there certificate to the signature on a user by user basis. + */ +public class SignatureAnnotationHandler extends SelectionBoxHandler + implements ToolHandler, MouseInputListener { + + private static final Logger logger = Logger.getLogger(SignatureAnnotationHandler.class.toString()); + + public SignatureAnnotationHandler(DocumentViewController documentViewController, + AbstractPageViewComponent pageViewComponent) { + super(documentViewController, pageViewComponent); + selectionBoxColour = Color.GRAY; + } + + public void mouseClicked(MouseEvent e) { + if (pageViewComponent != null) { + pageViewComponent.requestFocus(); + } + } + + public void mousePressed(MouseEvent e) { + // annotation selection box. + int x = e.getX(); + int y = e.getY(); + currentRect = new Rectangle(x, y, 0, 0); + updateDrawableRect(pageViewComponent.getWidth(), + pageViewComponent.getHeight()); + pageViewComponent.repaint(); + } + + public void mouseReleased(MouseEvent e) { + updateSelectionSize(e.getX(), e.getY(), pageViewComponent); + + // check the bounds on rectToDraw to try and avoid creating + // an annotation that is very small. + if (rectToDraw.getWidth() < 15 || rectToDraw.getHeight() < 15) { + rectToDraw.setSize(new Dimension(15, 15)); + } + + Rectangle tBbox = convertToPageSpace(rectToDraw).getBounds(); + + // create annotations types that are rectangle based; + // which is actually just link annotations + SignatureWidgetAnnotation annotation = (SignatureWidgetAnnotation) AnnotationFactory.buildWidgetAnnotation( + documentViewController.getDocument().getPageTree().getLibrary(), + FieldDictionaryFactory.TYPE_SIGNATURE, + tBbox); + // setup widget highlighting + ViewModel viewModel = documentViewController.getParentController().getViewModel(); + annotation.setEnableHighlightedWidget(viewModel.isWidgetAnnotationHighlight()); + + // Add the signatureWidget to catalog + InteractiveForm interactiveForm = + documentViewController.getDocument().getCatalog().getOrCreateInteractiveForm(); + interactiveForm.addField(annotation); + + // create the annotation object. + AbstractAnnotationComponent comp = + AnnotationComponentFactory.buildAnnotationComponent( + annotation, documentViewController, pageViewComponent); + comp.setBounds(rectToDraw); + comp.refreshAnnotationRect(); + + // add them to the container, using absolute positioning. + documentViewController.addNewAnnotation(comp); + + // set the annotation tool to the given tool + documentViewController.getParentController().setDocumentToolMode( + preferences.getInt(ViewerPropertiesManager.PROPERTY_ANNOTATION_SIGNATURE_SELECTION_TYPE, 0)); + + // clear the rectangle + clearRectangle(pageViewComponent); + + } + + protected void checkAndApplyPreferences() { + + } + + public void mouseEntered(MouseEvent e) { + + } + + public void mouseExited(MouseEvent e) { + + } + + public void mouseDragged(MouseEvent e) { + updateSelectionSize(e.getX(), e.getY(), pageViewComponent); + } + + public void mouseMoved(MouseEvent e) { + + } + + public void installTool() { + + } + + public void uninstallTool() { + + } + + @Override + public void setSelectionRectangle(Point cursorLocation, Rectangle selection) { + + } + + public void paintTool(Graphics g) { + paintSelectionBox(g, rectToDraw); + } + +} \ No newline at end of file diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FontWidgetUtilities.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FontWidgetUtilities.java new file mode 100644 index 000000000..38e4a6a38 --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FontWidgetUtilities.java @@ -0,0 +1,51 @@ +package org.icepdf.ri.common.utility.annotation.properties; + +import java.util.ResourceBundle; + +public class FontWidgetUtilities { + public static ValueLabelItem[] generateFontNameList(ResourceBundle messageBundle) { + return new ValueLabelItem[]{ + new ValueLabelItem("Helvetica", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.helvetica")), + new ValueLabelItem("Helvetica-Oblique", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.helveticaOblique")), + new ValueLabelItem("Helvetica-Bold", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.helveticaBold")), + new ValueLabelItem("Helvetica-BoldOblique", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name" + + ".HelveticaBoldOblique")), + new ValueLabelItem("Times-Italic", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesItalic")), + new ValueLabelItem("Times-Bold", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesBold")), + new ValueLabelItem("Times-BoldItalic", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesBoldItalic")), + new ValueLabelItem("Times-Roman", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesRoman")), + new ValueLabelItem("Courier", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courier")), + new ValueLabelItem("Courier-Oblique", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courierOblique")), + new ValueLabelItem("Courier-BoldOblique", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courierBoldOblique")), + new ValueLabelItem("Courier-Bold", + messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courierBold"))}; + } + + public static ValueLabelItem[] generateFontSizeNameList(ResourceBundle messageBundle) { + return new ValueLabelItem[]{ + new ValueLabelItem(6, messageBundle.getString("viewer.common.number.six")), + new ValueLabelItem(8, messageBundle.getString("viewer.common.number.eight")), + new ValueLabelItem(9, messageBundle.getString("viewer.common.number.nine")), + new ValueLabelItem(10, messageBundle.getString("viewer.common.number.ten")), + new ValueLabelItem(11, messageBundle.getString("viewer.common.number.eleven")), + new ValueLabelItem(12, messageBundle.getString("viewer.common.number.twelve")), + new ValueLabelItem(14, messageBundle.getString("viewer.common.number.fourteen")), + new ValueLabelItem(16, messageBundle.getString("viewer.common.number.sixteen")), + new ValueLabelItem(18, messageBundle.getString("viewer.common.number.eighteen")), + new ValueLabelItem(20, messageBundle.getString("viewer.common.number.twenty")), + new ValueLabelItem(24, messageBundle.getString("viewer.common.number.twentyFour")), + new ValueLabelItem(36, messageBundle.getString("viewer.common.number.thirtySix")), + new ValueLabelItem(48, messageBundle.getString("viewer.common.number.fortyEight"))}; + } +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FreeTextAnnotationPanel.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FreeTextAnnotationPanel.java index 6843f3d8c..fc3503ed6 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FreeTextAnnotationPanel.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/annotation/properties/FreeTextAnnotationPanel.java @@ -33,7 +33,6 @@ import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; -import java.util.ResourceBundle; /** * FreeTextAnnotationPanel is a configuration panel for changing the properties @@ -262,62 +261,17 @@ public void stateChanged(ChangeEvent e) { alphaSliderChange(e, freeTextAnnotation, ViewerPropertiesManager.PROPERTY_ANNOTATION_FREE_TEXT_OPACITY); } - public static ValueLabelItem[] generateFontNameList(ResourceBundle messageBundle) { - return new ValueLabelItem[]{ - new ValueLabelItem("Helvetica", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.helvetica")), - new ValueLabelItem("Helvetica-Oblique", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.helveticaOblique")), - new ValueLabelItem("Helvetica-Bold", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.helveticaBold")), - new ValueLabelItem("Helvetica-BoldOblique", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.HelveticaBoldOblique")), - new ValueLabelItem("Times-Italic", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesItalic")), - new ValueLabelItem("Times-Bold", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesBold")), - new ValueLabelItem("Times-BoldItalic", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesBoldItalic")), - new ValueLabelItem("Times-Roman", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.timesRoman")), - new ValueLabelItem("Courier", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courier")), - new ValueLabelItem("Courier-Oblique", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courierOblique")), - new ValueLabelItem("Courier-BoldOblique", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courierBoldOblique")), - new ValueLabelItem("Courier-Bold", - messageBundle.getString("viewer.utilityPane.annotation.freeText.font.name.courierBold"))}; - } - - public static ValueLabelItem[] generateFontSizeNameList(ResourceBundle messageBundle) { - return new ValueLabelItem[]{ - new ValueLabelItem(6, messageBundle.getString("viewer.common.number.six")), - new ValueLabelItem(8, messageBundle.getString("viewer.common.number.eight")), - new ValueLabelItem(9, messageBundle.getString("viewer.common.number.nine")), - new ValueLabelItem(10, messageBundle.getString("viewer.common.number.ten")), - new ValueLabelItem(11, messageBundle.getString("viewer.common.number.eleven")), - new ValueLabelItem(12, messageBundle.getString("viewer.common.number.twelve")), - new ValueLabelItem(14, messageBundle.getString("viewer.common.number.fourteen")), - new ValueLabelItem(16, messageBundle.getString("viewer.common.number.sixteen")), - new ValueLabelItem(18, messageBundle.getString("viewer.common.number.eighteen")), - new ValueLabelItem(20, messageBundle.getString("viewer.common.number.twenty")), - new ValueLabelItem(24, messageBundle.getString("viewer.common.number.twentyFour")), - new ValueLabelItem(36, messageBundle.getString("viewer.common.number.thirtySix")), - new ValueLabelItem(48, messageBundle.getString("viewer.common.number.fortyEight"))}; - } - private void createGUI() { // font styles - core java font names and respective labels. All Java JRE should have these fonts, these // fonts also have huge number of glyphs support many different languages. if (FONT_NAMES_LIST == null) { - FONT_NAMES_LIST = generateFontNameList(messageBundle); + FONT_NAMES_LIST = FontWidgetUtilities.generateFontNameList(messageBundle); } // Font size. if (FONT_SIZES_LIST == null) { - FONT_SIZES_LIST = generateFontSizeNameList(messageBundle); + FONT_SIZES_LIST = FontWidgetUtilities.generateFontSizeNameList(messageBundle); } // Create and setup an Appearance panel diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureTreeNode.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureTreeNode.java index b9967bb58..d28865c2f 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureTreeNode.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureTreeNode.java @@ -22,6 +22,7 @@ import org.icepdf.core.pobjects.acroform.SignatureFieldDictionary; import org.icepdf.core.pobjects.acroform.signature.SignatureValidator; import org.icepdf.core.pobjects.acroform.signature.exceptions.SignatureIntegrityException; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; import org.icepdf.ri.images.IconPack; import org.icepdf.ri.images.Images; diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureUtilities.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureUtilities.java deleted file mode 100644 index 1285528b1..000000000 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/SignatureUtilities.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2006-2019 ICEsoft Technologies Canada Corp. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an "AS - * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -package org.icepdf.ri.common.utility.signatures; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; - -/** - * Utility of commonly used signature related algorithms. - */ -public class SignatureUtilities { - - /** - * Parse out a known data element from an X500Name. - * - * @param rdName name to parse value from. - * @param commonCode BCStyle name . - * @return BCStyle name value, null if the BCStyle name was not found. - */ - public static String parseRelativeDistinguishedName(X500Name rdName, ASN1ObjectIdentifier commonCode) { - RDN[] rdns = rdName.getRDNs(commonCode); - if (rdns != null && rdns.length > 0 && rdns[0].getFirst() != null) { - return rdns[0].getFirst().getValue().toString(); - } - return null; - } -} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/VerifyAllSignaturesTask.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/VerifyAllSignaturesTask.java index d2e78b11a..d0df14894 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/VerifyAllSignaturesTask.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/utility/signatures/VerifyAllSignaturesTask.java @@ -64,6 +64,7 @@ protected Void doInBackground() { taskStatusMessage = messageFormat.format(new Object[]{i + 1, signatures.size()}); SignatureWidgetAnnotation signatureWidgetAnnotation = signatures.get(i); + signatureWidgetAnnotation.init(); SignatureDictionary signatureDictionary = signatureWidgetAnnotation.getSignatureDictionary(); if (signatureDictionary.getEntries().size() > 0) { try { diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewController.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewController.java index 9eba22d37..101210c06 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewController.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewController.java @@ -18,13 +18,12 @@ import org.icepdf.core.SecurityCallback; import org.icepdf.core.pobjects.Destination; import org.icepdf.core.pobjects.Document; -import org.icepdf.ri.common.views.annotations.AbstractAnnotationComponent; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureAppearanceCallback; import javax.swing.*; import java.awt.*; import java.awt.event.KeyListener; import java.util.Collection; -import java.util.Set; /** @@ -186,6 +185,8 @@ public interface DocumentViewController { void setAnnotationCallback(AnnotationCallback annotationCallback); + void setSignatureAppearanceCallback(SignatureAppearanceCallback signatureAppearanceCallback); + void setSecurityCallback(SecurityCallback securityCallback); void addNewAnnotation(AnnotationComponent annotationComponent); @@ -210,6 +211,8 @@ public interface DocumentViewController { AnnotationCallback getAnnotationCallback(); + SignatureAppearanceCallback getSignatureAppearanceCallback(); + SecurityCallback getSecurityCallback(); DocumentViewModel getDocumentViewModel(); diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewControllerImpl.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewControllerImpl.java index 40ff86652..fa3a3f742 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewControllerImpl.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewControllerImpl.java @@ -19,6 +19,7 @@ import org.icepdf.core.Memento; import org.icepdf.core.SecurityCallback; import org.icepdf.core.pobjects.*; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureAppearanceCallback; import org.icepdf.core.search.DocumentSearchController; import org.icepdf.core.util.Library; import org.icepdf.core.util.PropertyConstants; @@ -116,6 +117,7 @@ public class DocumentViewControllerImpl protected final SwingController viewerController; protected AnnotationCallback annotationCallback; + protected SignatureAppearanceCallback signatureAppearanceCallback; protected SecurityCallback securityCallback; protected final PropertyChangeSupport changes = new PropertyChangeSupport(this); @@ -226,7 +228,7 @@ public JViewport getViewPort() { } /** - * Set an annotation callback. + * Set a SignatureAppearanceCallback callback. Allows setting up a custom signature appearance stream * * @param annotationCallback annotation callback associated with this document * view. @@ -352,6 +354,25 @@ public AnnotationCallback getAnnotationCallback() { return annotationCallback; } + + /** + * Gets the SignatureAppearanceCallback used to generate a signature annotation's appearance stream + * + * @return assigned callback + */ + public SignatureAppearanceCallback getSignatureAppearanceCallback() { + return signatureAppearanceCallback; + } + + /** + * Set a SignatureAppearanceCallback callback. Allows setting up a custom signature appearance stream + * + * @return annotation callback associated with this document. + */ + public void setSignatureAppearanceCallback(SignatureAppearanceCallback signatureAppearanceCallback) { + this.signatureAppearanceCallback = signatureAppearanceCallback; + } + /** * Gets the security callback. * @@ -899,8 +920,7 @@ public boolean setToolMode(final int viewToolMode) { if (documentView != null) documentView.setToolMode(viewToolMode); // notify the page components of the tool change. - List pageComponents = - documentViewModel.getPageComponents(); + List pageComponents = documentViewModel.getPageComponents(); for (AbstractPageViewComponent page : pageComponents) { ((PageViewComponentImpl) page).setToolMode(viewToolMode); } diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewModel.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewModel.java index 16035fdf0..216ab3fc1 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewModel.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/DocumentViewModel.java @@ -116,6 +116,8 @@ public interface DocumentViewModel { int DISPLAY_TOOL_REDACTION_ANNOTATION = 19; + int DISPLAY_TOOL_SIGNATURE_ANNOTATION = 20; + /** * Display tool constant for setting no tools */ diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/PageViewComponentImpl.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/PageViewComponentImpl.java index f27a3bef3..35df9018d 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/PageViewComponentImpl.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/PageViewComponentImpl.java @@ -132,43 +132,39 @@ public void setToolMode(final int viewToolMode) { this); break; case DocumentViewModel.DISPLAY_TOOL_SELECTION: - // no handler is needed for selection as it is handle by - // each annotation. + // no handler is needed for selection as it is handled by each annotation. currentToolHandler = new AnnotationSelectionHandler( documentViewController, this); documentViewController.clearSelectedText(); break; case DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION: - // handler is responsible for the initial creation of the annotation currentToolHandler = new LinkAnnotationHandler( documentViewController, this); documentViewController.clearSelectedText(); break; case DocumentViewModel.DISPLAY_TOOL_HIGHLIGHT_ANNOTATION: - // handler is responsible for the initial creation of the annotation currentToolHandler = new HighLightAnnotationHandler( documentViewController, this); - ((HighLightAnnotationHandler) currentToolHandler).createMarkupAnnotation(null); documentViewController.clearSelectedText(); break; case DocumentViewModel.DISPLAY_TOOL_REDACTION_ANNOTATION: - // handler is responsible for the initial creation of the annotation currentToolHandler = new RedactionAnnotationHandler( documentViewController, this); - ((RedactionAnnotationHandler) currentToolHandler).createMarkupAnnotation(null); documentViewController.clearSelectedText(); break; - + case DocumentViewModel.DISPLAY_TOOL_SIGNATURE_ANNOTATION: + currentToolHandler = new SignatureAnnotationHandler( + documentViewController, this); + documentViewController.clearSelectedText(); + break; case DocumentViewModel.DISPLAY_TOOL_STRIKEOUT_ANNOTATION: currentToolHandler = new StrikeOutAnnotationHandler( documentViewController, this); - ((StrikeOutAnnotationHandler) currentToolHandler).createMarkupAnnotation(null); documentViewController.clearSelectedText(); break; case DocumentViewModel.DISPLAY_TOOL_UNDERLINE_ANNOTATION: currentToolHandler = new UnderLineAnnotationHandler( documentViewController, this); - ((UnderLineAnnotationHandler) currentToolHandler).createMarkupAnnotation(null); documentViewController.clearSelectedText(); break; case DocumentViewModel.DISPLAY_TOOL_LINE_ANNOTATION: diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/LinkAnnotationComponent.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/LinkAnnotationComponent.java index bfa39d1cd..681ba2277 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/LinkAnnotationComponent.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/LinkAnnotationComponent.java @@ -75,10 +75,10 @@ private boolean isAnnotationEditable() { } public void paintComponent(Graphics g) { - // sniff out tool bar state to set correct annotation border + // sniff out toolbar state to set correct annotation border isEditable = isAnnotationEditable(); - // check for the annotation editing mode and draw the link effect so it's easier to see. + // check for the annotation editing mode and draw the link effect, so it's easier to see. if (documentViewController.getParentController().getViewModel().isAnnotationEditingMode()) { Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/acroform/SignatureComponent.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/acroform/SignatureComponent.java index 35abe5e59..56bc785c8 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/acroform/SignatureComponent.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/acroform/SignatureComponent.java @@ -21,18 +21,21 @@ import org.icepdf.core.pobjects.acroform.signature.SignatureValidator; import org.icepdf.core.pobjects.acroform.signature.exceptions.SignatureIntegrityException; import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.core.util.Library; import org.icepdf.ri.common.views.AbstractPageViewComponent; import org.icepdf.ri.common.views.Controller; import org.icepdf.ri.common.views.DocumentViewController; import org.icepdf.ri.common.views.annotations.AbstractAnnotationComponent; import org.icepdf.ri.common.views.annotations.signatures.CertificatePropertiesDialog; import org.icepdf.ri.common.views.annotations.signatures.SignaturePropertiesDialog; +import org.icepdf.ri.common.views.annotations.signing.SignatureCreationDialog; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -46,7 +49,10 @@ public class SignatureComponent extends AbstractAnnotationComponent { + + protected static final Logger logger = + Logger.getLogger(BasicSignatureAppearanceCallback.class.toString()); + + protected SignatureAppearanceModelImpl signatureAppearanceModel; + + + @Override + public void setSignatureAppearanceModel(SignatureAppearanceModelImpl signatureAppearanceModel) { + this.signatureAppearanceModel = signatureAppearanceModel; + } + + @Override + public void removeAppearanceStream(SignatureWidgetAnnotation signatureWidgetAnnotation, + AffineTransform pageSpace, boolean isNew) { + if (signatureAppearanceModel == null) { + throw new IllegalStateException("SignatureAppearanceModel must be set before calling this method."); + } + Library library = signatureWidgetAnnotation.getLibrary(); + SignatureManager signatureManager = library.getSignatureDictionaries(); + signatureWidgetAnnotation.setSignatureDictionary(new SignatureDictionary(library, new DictionaryEntries())); + signatureManager.clearSignatures(); + StateManager stateManager = library.getStateManager(); + stateManager.removeChange(new PObject(null, signatureAppearanceModel.getImageXObjectReference())); + + Name currentAppearance = signatureWidgetAnnotation.getCurrentAppearance(); + HashMap appearances = signatureWidgetAnnotation.getAppearances(); + Appearance appearance = appearances.get(currentAppearance); + AppearanceState appearanceState = appearance.getSelectedAppearanceState(); + Shapes shapes = ContentWriterUtils.createAppearanceShapes(appearanceState, 0, 0); + byte[] postScript = PostScriptEncoder.generatePostScript(shapes.getShapes()); + Rectangle2D bbox = appearanceState.getBbox(); + AffineTransform matrix = appearanceState.getMatrix(); + Form xObject = signatureWidgetAnnotation.updateAppearanceStream(shapes, bbox, matrix, postScript, isNew); + xObject.getEntries().remove(RESOURCES_KEY); + } + + @Override + public void createAppearanceStream(SignatureWidgetAnnotation signatureWidgetAnnotation, + AffineTransform pageSpace, boolean isNew) { + if (signatureAppearanceModel == null) { + throw new IllegalStateException("SignatureAppearanceModel must be set before calling this method."); + } + SignatureDictionary signatureDictionary = signatureWidgetAnnotation.getSignatureDictionary(); + Name currentAppearance = signatureWidgetAnnotation.getCurrentAppearance(); + HashMap appearances = signatureWidgetAnnotation.getAppearances(); + Appearance appearance = appearances.get(currentAppearance); + AppearanceState appearanceState = appearance.getSelectedAppearanceState(); + + Shapes shapes = ContentWriterUtils.createAppearanceShapes(appearanceState, 0, 0); + + if (!signatureAppearanceModel.isSignatureVisible() || !signatureAppearanceModel.isSelectedCertificate()) { + return; + } + + // create the new font to draw with + FontFile fontFile = ContentWriterUtils.createFont(signatureAppearanceModel.getFontName()); + fontFile = fontFile.deriveFont(signatureAppearanceModel.getFontSize()); + + ResourceBundle messageBundle = signatureAppearanceModel.getMessageBundle(); + + Library library = signatureDictionary.getLibrary(); + + // reasons + MessageFormat reasonFormatter = new MessageFormat(messageBundle.getString( + "viewer.annotation.signature.handler.properties.reason.label")); + String reasonTranslated; + if (signatureAppearanceModel.getSignatureType() == SignatureType.CERTIFIER) { + reasonTranslated = messageBundle.getString( + "viewer.annotation.signature.handler.properties.reason.certification.label"); + } else { + reasonTranslated = messageBundle.getString( + "viewer.annotation.signature.handler.properties.reason.approval.label"); + } + String reason = reasonFormatter.format(new Object[]{reasonTranslated}); + // contact info + MessageFormat contactFormatter = new MessageFormat(messageBundle.getString( + "viewer.annotation.signature.handler.properties.contact.label")); + String contactInfo = contactFormatter.format(new Object[]{signatureAppearanceModel.getContact()}); + // common name + MessageFormat signerFormatter = new MessageFormat(messageBundle.getString( + "viewer.annotation.signature.handler.properties.signer.label")); + String commonName = signerFormatter.format(new Object[]{signatureAppearanceModel.getName()}); + // location + MessageFormat locationFormatter = new MessageFormat(messageBundle.getString( + "viewer.annotation.signature.handler.properties.location.label")); + String location = locationFormatter.format(new Object[]{signatureAppearanceModel.getLocation()}); + + Rectangle2D bbox = appearanceState.getBbox(); + + // create new image stream for the signature image + BufferedImage signatureImage = signatureAppearanceModel.getSignatureImage(); + Name imageName = signatureAppearanceModel.getImageXObjectName(); + Reference imageReference = signatureAppearanceModel.getImageXObjectReference(); + ImageStream imageStream = null; + if (signatureAppearanceModel.isSignatureImageVisible() && signatureImage != null) { + imageStream = ContentWriterUtils.addImageToShapes(library, imageName, imageReference, signatureImage, + shapes, bbox, signatureAppearanceModel.getImageScale()); + signatureAppearanceModel.setImageXObjectReference(imageStream.getPObjectReference()); + } + + if (signatureAppearanceModel.isSignatureTextVisible()) { + float offsetY = 0; + int lineSpacing = signatureAppearanceModel.getFontSize(); + int fontSize = signatureAppearanceModel.getFontSize(); + + String[] signatureText = {reason, contactInfo, commonName, location}; + int leftMargin = calculateLeftMargin(bbox, signatureText); + int padding = 3; + float groupSpacing = calculateTextSpacing(bbox, signatureText, fontSize, padding); + AffineTransform centeringTransform = calculatePaddingTransform(leftMargin, padding); + + Point2D.Float lastOffset; + float advanceY = (float) bbox.getMinY() + offsetY; + shapes.add(new TransformDrawCmd(centeringTransform)); + for (String text : signatureText) { + lastOffset = ContentWriterUtils.addTextSpritesToShapes(fontFile, 0, advanceY, + shapes, + fontSize, + lineSpacing, + signatureAppearanceModel.getFontColor(), + text); + advanceY = lastOffset.y + groupSpacing; + } + } + + // finalized appearance stream and generated postscript + StateManager stateManager = library.getStateManager(); + AffineTransform matrix = appearanceState.getMatrix(); + + byte[] postScript = PostScriptEncoder.generatePostScript(shapes.getShapes()); + Form xObject = signatureWidgetAnnotation.updateAppearanceStream(shapes, bbox, matrix, postScript, isNew); + xObject.addFontResource(ContentWriterUtils.createDefaultFontDictionary(signatureAppearanceModel.getFontName())); + if (signatureAppearanceModel.isSignatureImageVisible() && imageStream != null) { + xObject.addImageResource(imageName, imageStream); + } + try { + xObject.init(); + // the image make it more difficult to use the shapes array, so we generated + // from the postscript array to get a proper shapes + appearanceState.setShapes(xObject.getShapes()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + ContentWriterUtils.setAppearance(signatureWidgetAnnotation, xObject, appearanceState, stateManager, isNew); + + } + + private float calculateTextSpacing(Rectangle2D bbox, String[] text, int fontSize, int padding) { + float textHeight = text.length * fontSize; + float bboxHeight = (float) bbox.getHeight() - (padding * 2); + if (textHeight > bboxHeight) { + return 0; + } else { + return (bboxHeight - textHeight) / (text.length - 1); + } + } + + private AffineTransform calculatePaddingTransform(int leftMargin, int padding) { + // this is a little fuzzy but should work for most cases to center text in the middle of the bbox0; + return new AffineTransform( + 1, 0, 0, + 1, leftMargin, + padding); + } + + private int calculateLeftMargin(Rectangle2D bbox, String[] text) { + Font font = new Font(signatureAppearanceModel.getFontName(), Font.PLAIN, + signatureAppearanceModel.getFontSize()); + FontRenderContext fontRenderContext = new FontRenderContext(new AffineTransform(), true, true); + int maxWidth = 0; + + for (String s : text) { + GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, s); + Rectangle2D rect = glyphVector.getOutline().getBounds2D(); + if (rect.getWidth() > maxWidth) { + maxWidth = (int) rect.getWidth(); + } + } + return (int) bbox.getWidth() - maxWidth; + } + +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/CertificateTableModel.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/CertificateTableModel.java new file mode 100644 index 000000000..53b91c91c --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/CertificateTableModel.java @@ -0,0 +1,103 @@ +package org.icepdf.ri.common.views.annotations.signing; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.icepdf.core.pobjects.acroform.signature.handlers.SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; + +import javax.security.auth.x500.X500Principal; +import javax.swing.table.AbstractTableModel; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.*; + +public class CertificateTableModel extends AbstractTableModel { + + private String[] columnNames; + private String[][] data = new String[][]{}; + private ArrayList certs; + private ArrayList aliases; + private static SimpleDateFormat validityDateFormat = new SimpleDateFormat("dd/MM/yyyy"); + + public CertificateTableModel(SignerHandler signerHandler, Enumeration aliases, + ResourceBundle messageBundle) throws KeyStoreException { + columnNames = new String[]{ + messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.table.name.label"), + messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.table.author.label"), + messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.table.validity.label"), + messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.table.description.label")}; + + // build data from aliases in keystore. + List rows = new ArrayList<>(); + certs = new ArrayList<>(); + this.aliases = Collections.list(aliases); + for (String alias : this.aliases) { + X509Certificate cert = signerHandler.getCertificate(alias); + certs.add(cert); + rows.add(createCertSummaryData(cert)); + } + data = new String[rows.size()][columnNames.length]; + rows.toArray(data); + + } + + private static String[] createCertSummaryData(X509Certificate certificate) { + X500Principal principal = certificate.getSubjectX500Principal(); + X500Name x500name = new X500Name(principal.getName()); + // Set up dictionary using certificate values. + // https://javadoc.io/static/org.bouncycastle/bcprov-jdk15on/1.70/org/bouncycastle/asn1/x500/style/BCStyle.html + if (x500name.getRDNs() != null) { + String commonName = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.CN); + String email = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.EmailAddress); + String validity = validityDateFormat.format(certificate.getNotAfter()); + String description = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.DESCRIPTION); + return new String[]{commonName, email, validity, description,}; + } else { + throw new IllegalStateException("Certificate has no DRNs data"); + } + } + + @Override + public int getRowCount() { + return data.length; + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public String getColumnName(int col) { + return columnNames[col]; + } + + public String getAliasAt(int row) { + if (row < 0 || row >= getRowCount()) { + return null; + } + return aliases.get(row); + } + + public X509Certificate getCertificateAt(int row) { + if (row < 0 || row >= getRowCount()) { + return null; + } + return certs.get(row); + } + + @Override + public Object getValueAt(int row, int col) { + return data[row][col]; + } + + @Override + public boolean isCellEditable(int row, int col) { + return false; + } +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/PasswordDialogCallbackHandler.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/PasswordDialogCallbackHandler.java new file mode 100644 index 000000000..b44bd53ce --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/PasswordDialogCallbackHandler.java @@ -0,0 +1,96 @@ +package org.icepdf.ri.common.views.annotations.signing; + +import org.icepdf.core.pobjects.acroform.signature.handlers.PasswordCallbackHandler; + +import javax.security.auth.callback.*; +import javax.swing.*; +import java.io.IOException; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static javax.swing.JOptionPane.CLOSED_OPTION; +import static javax.swing.JOptionPane.OK_OPTION; +import static org.icepdf.ri.common.preferences.SigningPreferencesPanel.PKCS_11_TYPE; + +/** + * PasswordDialogCallbackHandler handles requesting passwords or pins when accessing a users keystore. The password + * is used to open the keystore as well as retrieve the private key used when signing a document. + * + * @since 7.3 + */ +public class PasswordDialogCallbackHandler extends PasswordCallbackHandler { + + private static final Logger logger = Logger.getLogger(PasswordDialogCallbackHandler.class.getName()); + + private JDialog parentComponent; + private ResourceBundle messageBundle; + private String dialogType; + + public PasswordDialogCallbackHandler(JDialog parentDialog, ResourceBundle messageBundle) { + super(""); + this.parentComponent = parentDialog; + this.messageBundle = messageBundle; + } + + public void setType(String dialogType) { + this.dialogType = dialogType; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback pc = (PasswordCallback) callback; +// pc.setPassword("changeit".toCharArray()); +// password = "changeit"; + JPanel panel = new JPanel(); + String[] options = new String[]{ + messageBundle.getString("viewer.button.ok.label"), + messageBundle.getString("viewer.button.cancel.label")}; + String dialogTitle; + // slightly different verbiage for pkcs11 or pks12. + if (dialogType.equals(PKCS_11_TYPE)) { + dialogTitle = messageBundle.getString( + "viewer.annotation.signature.creation.keystore.pkcs11.dialog.title"); + JLabel label = new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.keystore.pkcs11.dialog.label")); + panel.add(label); + } else { + dialogTitle = messageBundle.getString( + "viewer.annotation.signature.creation.keystore.pkcs12.dialog.title"); + JLabel label = new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.keystore.pkcs12.dialog.label")); + panel.add(label); + } + JPasswordField pass = new JPasswordField(15); + panel.add(pass); + int option = JOptionPane.showOptionDialog(parentComponent, panel, + dialogTitle, + JOptionPane.YES_NO_OPTION, JOptionPane.PLAIN_MESSAGE, + null, options, options[0]); + if (option == OK_OPTION) { + char[] password = pass.getPassword(); + this.password = password; + pc.setPassword(password); + } else if (option == CLOSED_OPTION) { + System.out.println("closed"); + } + + } else if (callback instanceof TextOutputCallback) { + TextOutputCallback tc = (TextOutputCallback) callback; + logger.log(Level.WARNING, + "TextOutputCallback type {0} message: {1}", + new Object[]{tc.getMessageType(), tc.getMessage()}); + throw new UnsupportedCallbackException(callback); + } else if (callback instanceof NameCallback) { + throw new UnsupportedCallbackException(callback); + } else { + logger.log(Level.WARNING, + "Unknown callback type {0}", + callback.getClass().getName()); + throw new UnsupportedCallbackException(callback); + } + } + } +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/PkcsSignerFactory.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/PkcsSignerFactory.java new file mode 100644 index 000000000..1467ef3ef --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/PkcsSignerFactory.java @@ -0,0 +1,47 @@ +package org.icepdf.ri.common.views.annotations.signing; + +import org.icepdf.core.pobjects.acroform.signature.handlers.Pkcs11SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.handlers.Pkcs12SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.handlers.SignerHandler; +import org.icepdf.ri.util.ViewerPropertiesManager; + +import java.io.File; +import java.util.prefs.Preferences; + +import static org.icepdf.ri.common.preferences.SigningPreferencesPanel.PKCS_11_TYPE; +import static org.icepdf.ri.common.preferences.SigningPreferencesPanel.PKCS_12_TYPE; + +/** + * Factory class for creating a SignerHandler instance based on the keystore type. + * + * @since 7.3 + */ +public class PkcsSignerFactory { + + private PkcsSignerFactory() { + } + + /** + * Factory method for creating a SignerHandler instance based on the keystore type. Two instance types are supported + * PKCS12 and PKCS11. + * + * @param passwordDialogCallbackHandler + * @return SignerHandler instance based on the keystore type. Null if the keystore type is not supported. + */ + public static SignerHandler getInstance(PasswordDialogCallbackHandler passwordDialogCallbackHandler) { + ViewerPropertiesManager propertiesManager = ViewerPropertiesManager.getInstance(); + Preferences preferences = propertiesManager.getPreferences(); + String keyStoreType = preferences.get(ViewerPropertiesManager.PROPERTY_PKCS_KEYSTORE_TYPE, ""); + if (keyStoreType.equals(PKCS_12_TYPE)) { + passwordDialogCallbackHandler.setType(PKCS_12_TYPE); + String keyStorePath = preferences.get(ViewerPropertiesManager.PROPERTY_PKCS12_PROVIDER_KEYSTORE_PATH, ""); + File keystoreFile = new File(keyStorePath); + return new Pkcs12SignerHandler(keystoreFile, null, passwordDialogCallbackHandler); + } else if (keyStoreType.equals(PKCS_11_TYPE)) { + passwordDialogCallbackHandler.setType(PKCS_11_TYPE); + String configPath = preferences.get(ViewerPropertiesManager.PROPERTY_PKCS11_PROVIDER_CONFIG_PATH, ""); + return new Pkcs11SignerHandler(configPath, null, passwordDialogCallbackHandler); + } + return null; + } +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/SignatureAppearanceModelImpl.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/SignatureAppearanceModelImpl.java new file mode 100644 index 000000000..71ec35e70 --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/SignatureAppearanceModelImpl.java @@ -0,0 +1,189 @@ +package org.icepdf.ri.common.views.annotations.signing; + +import org.icepdf.core.pobjects.Name; +import org.icepdf.core.pobjects.Reference; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureAppearanceModel; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureType; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; +import org.icepdf.core.util.Library; +import org.icepdf.ri.util.ViewerPropertiesManager; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.prefs.Preferences; + + +/** + * Signature appearance state allows the signature builder to dialog/ui and a SignatureAppearanceCallback + * implementation to + * share a common model. When any of the model property values are changed a + * SIGNATURE_ANNOTATION_APPEARANCE_PROPERTY_CHANGE + * even is fired. The intent is that any properties chane can trigger the SignatureAppearanceCallback to rebuild + * the signatures appearance stream. + */ +public class SignatureAppearanceModelImpl implements SignatureAppearanceModel { + + private BufferedImage signatureImage; + private Name imageXObjectName; + private Reference imageXObjectReference; + + private Color fontColor = Color.BLACK; + + private SignatureType signatureType; + private boolean signatureVisible = true; + private boolean isSelectedCertificate = true; + private String location; + private String contact; + private String name; + + private ResourceBundle messageBundle; + private Locale locale; + private final Preferences preferences; + + + public SignatureAppearanceModelImpl(Library library) { + imageXObjectName = new Name("sig_img_" + library.getStateManager().getNextImageNumber()); + preferences = ViewerPropertiesManager.getInstance().getPreferences(); + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + messageBundle = ResourceBundle.getBundle(ViewerPropertiesManager.DEFAULT_MESSAGE_BUNDLE, locale); + } + + public Name getImageXObjectName() { + return imageXObjectName; + } + + public Reference getImageXObjectReference() { + return imageXObjectReference; + } + + public void setImageXObjectReference(Reference imageXObjectReference) { + this.imageXObjectReference = imageXObjectReference; + } + + public SignatureType getSignatureType() { + return signatureType; + } + + public void setSignatureType(SignatureType signatureType) { + this.signatureType = signatureType; + } + + public boolean isSignatureVisible() { + return signatureVisible; + } + + public boolean isSelectedCertificate() { + return isSelectedCertificate; + } + + public void setSelectedCertificate(boolean selectedCertificate) { + isSelectedCertificate = selectedCertificate; + } + + public void setSignatureVisible(boolean signatureVisible) { + this.signatureVisible = signatureVisible; + } + + public BufferedImage getSignatureImage() { + return signatureImage; + } + + public void setSignatureImage(BufferedImage image) { + this.signatureImage = image; + } + + public String getFontName() { + return preferences.get(ViewerPropertiesManager.PROPERTY_SIGNATURE_FONT_NAME, "Helvetica"); + } + + public void setFontName(String fontName) { + preferences.put(ViewerPropertiesManager.PROPERTY_SIGNATURE_FONT_NAME, fontName); + } + + public int getFontSize() { + return preferences.getInt(ViewerPropertiesManager.PROPERTY_SIGNATURE_FONT_SIZE, 6); + } + + public void setFontSize(int fontSize) { + preferences.putInt(ViewerPropertiesManager.PROPERTY_SIGNATURE_FONT_SIZE, fontSize); + } + + public boolean isSignatureTextVisible() { + return preferences.getBoolean(ViewerPropertiesManager.PROPERTY_SIGNATURE_SHOW_TEXT, true); + } + + public void setSignatureTextVisible(boolean signatureTextVisible) { + preferences.putBoolean(ViewerPropertiesManager.PROPERTY_SIGNATURE_SHOW_TEXT, signatureTextVisible); + } + + public boolean isSignatureImageVisible() { + return preferences.getBoolean(ViewerPropertiesManager.PROPERTY_SIGNATURE_SHOW_IMAGE, true); + } + + public void setSignatureImageVisible(boolean signatureImageVisible) { + preferences.putBoolean(ViewerPropertiesManager.PROPERTY_SIGNATURE_SHOW_IMAGE, signatureImageVisible); + } + + public int getImageScale() { + return preferences.getInt(ViewerPropertiesManager.PROPERTY_SIGNATURE_IMAGE_SCALE, 100); + } + + public void setImageScale(int imageScale) { + preferences.putInt(ViewerPropertiesManager.PROPERTY_SIGNATURE_IMAGE_SCALE, imageScale); + } + + public void setSignatureImagePath(String imagePath) { + preferences.put(ViewerPropertiesManager.PROPERTY_SIGNATURE_IMAGE_PATH, imagePath); + signatureImage = SignatureUtilities.loadSignatureImage(imagePath); + } + + public String getSignatureImagePath() { + return preferences.get(ViewerPropertiesManager.PROPERTY_SIGNATURE_IMAGE_PATH, ""); + } + + public Color getFontColor() { + return fontColor; + } + + public void setFontColor(Color fontColor) { + this.fontColor = fontColor; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getContact() { + return contact; + } + + public void setContact(String contact) { + this.contact = contact; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ResourceBundle getMessageBundle() { + return messageBundle; + } + +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/SignatureCreationDialog.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/SignatureCreationDialog.java new file mode 100644 index 000000000..e64183122 --- /dev/null +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/signing/SignatureCreationDialog.java @@ -0,0 +1,650 @@ +package org.icepdf.ri.common.views.annotations.signing; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.icepdf.core.pobjects.acroform.SignatureDictionary; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureAppearanceCallback; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureType; +import org.icepdf.core.pobjects.acroform.signature.handlers.SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.core.util.Library; +import org.icepdf.core.util.SignatureManager; +import org.icepdf.ri.common.EscapeJDialog; +import org.icepdf.ri.common.utility.annotation.properties.FontWidgetUtilities; +import org.icepdf.ri.common.utility.annotation.properties.ValueLabelItem; +import org.icepdf.ri.common.views.Controller; +import org.icepdf.ri.common.views.annotations.acroform.SignatureComponent; + +import javax.security.auth.x500.X500Principal; +import javax.swing.*; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.AffineTransform; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Logger; + +/** + * The SignatureCreationDialog allows users to select an available signing certificate and customize various setting + * associated with signing a document. + */ +public class SignatureCreationDialog extends EscapeJDialog implements ActionListener, ListSelectionListener, + ItemListener, FocusListener, ChangeListener { + + private static final Logger logger = + Logger.getLogger(SignatureCreationDialog.class.toString()); + + private static final Locale[] supportedLocales = { + new Locale("da"), + new Locale("de"), + new Locale("en"), + new Locale("es"), + new Locale("fi"), + new Locale("fr"), + new Locale("it"), + new Locale("nl"), + new Locale("no"), + new Locale("pt"), + new Locale("sv"), + }; + + private GridBagConstraints constraints; + + private JTable certificateTable; + private JRadioButton signerRadioButton; + private JRadioButton certifyRadioButton; + private JCheckBox signerVisibilityCheckBox; + private JTextField locationTextField; + private JTextField nameTextField; + private JTextField contactTextField; + + private JComboBox fontNameBox; + private JComboBox fontSizeBox; + private JCheckBox showTextCheckBox; + private JCheckBox showSignatureCheckBox; + private JTextField imagePathTextField; + private JButton imagePathBrowseButton; + private JSlider imageScaleSlider; + + private JComboBox languagesComboBox; + private JButton signButton; + private JButton closeButton; + + private SignerHandler signerHandler; + + private final SignatureAppearanceCallback signatureAppearanceCallback; + private final SignatureAppearanceModelImpl signatureAppearanceModel; + + protected static ResourceBundle messageBundle; + protected final SignatureComponent signatureWidgetComponent; + protected final SignatureWidgetAnnotation signatureWidgetAnnotation; + + public SignatureCreationDialog(Controller controller, ResourceBundle messageBundle, + SignatureComponent signatureComponent) throws KeyStoreException { + super(controller.getViewerFrame(), true); + SignatureCreationDialog.messageBundle = messageBundle; + this.signatureWidgetComponent = signatureComponent; + this.signatureWidgetAnnotation = signatureComponent.getAnnotation(); + + signatureAppearanceCallback = controller.getDocumentViewController().getSignatureAppearanceCallback(); + signatureAppearanceModel = new SignatureAppearanceModelImpl(signatureComponent.getAnnotation().getLibrary()); + signatureAppearanceModel.setSelectedCertificate(false); + signatureAppearanceCallback.setSignatureAppearanceModel(signatureAppearanceModel); + signatureWidgetAnnotation.setAppearanceCallback(signatureAppearanceCallback); + + this.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + cancelOrCloseSignatureCleanup(); + } + }); + + buildUI(); + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + + Object source = actionEvent.getSource(); + if (source == null) return; + + if (source == signButton) { + Library library = signatureWidgetAnnotation.getLibrary(); + SignatureManager signatureManager = library.getSignatureDictionaries(); + + // set up signer dictionary as the primary certification signer. + SignatureDictionary signatureDictionary; + if (signerRadioButton.isSelected()) { + signatureDictionary = SignatureDictionary.getInstance(signatureWidgetAnnotation, SignatureType.SIGNER); + } else { + if (signatureManager.hasExistingCertifier(library)) { + JOptionPane.showMessageDialog(this, + messageBundle.getString("viewer.annotation.signature.creation.dialog.certify.error.msg"), + messageBundle.getString("viewer.annotation.signature.creation.dialog.certify.error.title"), + JOptionPane.ERROR_MESSAGE); + return; + } + signatureDictionary = SignatureDictionary.getInstance(signatureWidgetAnnotation, + SignatureType.CERTIFIER); + } + signatureDictionary.setSignerHandler(signerHandler); + signatureManager.addSignature(signatureDictionary, signatureWidgetAnnotation); + + // assign original values from cert + signatureDictionary.setName(nameTextField.getText()); + signatureDictionary.setContactInfo(contactTextField.getText()); + signatureDictionary.setLocation(locationTextField.getText()); + signatureDictionary.setReason(signerRadioButton.isSelected() ? + SignatureType.SIGNER.toString().toLowerCase() : + SignatureType.CERTIFIER.toString().toLowerCase()); + buildAppearanceStream(); + + setVisible(false); + dispose(); + } else if (source == closeButton) { + // clean anything we set up and just leave the signature with an empty dictionary + cancelOrCloseSignatureCleanup(); + setVisible(false); + dispose(); + } else if (source == imagePathTextField) { + setSignatureImage(); + buildAppearanceStream(); + } else if (source == signerRadioButton) { + signatureAppearanceModel.setSignatureType(SignatureType.SIGNER); + buildAppearanceStream(); + } else if (source == certifyRadioButton) { + signatureAppearanceModel.setSignatureType(SignatureType.CERTIFIER); + buildAppearanceStream(); + } else if (source == signerVisibilityCheckBox) { + signatureAppearanceModel.setSignatureVisible(signerVisibilityCheckBox.isSelected()); + buildAppearanceStream(); + } else if (source == showTextCheckBox) { + signatureAppearanceModel.setSignatureTextVisible(showTextCheckBox.isSelected()); + buildAppearanceStream(); + } else if (source == showSignatureCheckBox) { + signatureAppearanceModel.setSignatureImageVisible(showSignatureCheckBox.isSelected()); + buildAppearanceStream(); + } else if (source == imagePathBrowseButton) { + String imagePath = signatureAppearanceModel.getSignatureImagePath(); + JFileChooser fileChooser = new JFileChooser(imagePath); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setDialogTitle(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.selection.title")); + final int responseValue = fileChooser.showDialog(this, messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.selection.accept.label")); + if (responseValue == JFileChooser.APPROVE_OPTION) { + imagePathTextField.setText(fileChooser.getSelectedFile().getAbsolutePath()); + setSignatureImage(); + buildAppearanceStream(); + } + } + } + + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) { + return; + } + if (e.getSource() == fontSizeBox) { + ValueLabelItem item = (ValueLabelItem) fontSizeBox.getSelectedItem(); + if (item != null) { + int fontSize = (int) item.getValue(); + signatureAppearanceModel.setFontSize(fontSize); + buildAppearanceStream(); + } + } else if (e.getSource() == fontNameBox) { + ValueLabelItem item = (ValueLabelItem) fontNameBox.getSelectedItem(); + if (item != null) { + String fontName = item.getValue().toString(); + signatureAppearanceModel.setFontName(fontName); + buildAppearanceStream(); + } + } else if (e.getSource() == languagesComboBox) { + signatureAppearanceModel.setLocale((Locale) languagesComboBox.getSelectedItem()); + buildAppearanceStream(); + } + } + + @Override + public void stateChanged(ChangeEvent e) { + JSlider source = (JSlider) e.getSource(); + if (!source.getValueIsAdjusting()) { + int scale = source.getValue(); + signatureAppearanceModel.setImageScale(scale); + buildAppearanceStream(); + } + } + + @Override + public void valueChanged(ListSelectionEvent event) { + if (event.getValueIsAdjusting()) { + return; + } + int row = certificateTable.convertRowIndexToModel(certificateTable.getSelectedRow()); + CertificateTableModel model = (CertificateTableModel) certificateTable.getModel(); + signerHandler.setCertAlias(model.getAliasAt(row)); + setSelectedCertificate(model.getCertificateAt(row)); + buildAppearanceStream(); + } + + @Override + public void focusGained(FocusEvent focusEvent) { + + } + + @Override + public void focusLost(FocusEvent focusEvent) { + Object source = focusEvent.getSource(); + boolean changed = false; + if (source == locationTextField) { + signatureAppearanceModel.setLocation(locationTextField.getText()); + changed = true; + } else if (source == contactTextField) { + signatureAppearanceModel.setContact(contactTextField.getText()); + changed = true; + } else if (source == nameTextField) { + signatureAppearanceModel.setName(nameTextField.getText()); + changed = true; + } else if (source == imagePathTextField) { + setSignatureImage(); + changed = true; + } + if (changed) { + buildAppearanceStream(); + } + } + + private void cancelOrCloseSignatureCleanup() { + signatureAppearanceCallback.removeAppearanceStream(signatureWidgetAnnotation, new AffineTransform(), true); + signatureWidgetAnnotation.setAppearanceCallback(null); + } + + private void updateModelAppearanceState() { + signatureAppearanceModel.setLocation(locationTextField.getText()); + signatureAppearanceModel.setContact(contactTextField.getText()); + signatureAppearanceModel.setName(nameTextField.getText()); + signatureAppearanceModel.setSignatureType(signerRadioButton.isSelected() ? + SignatureType.SIGNER : SignatureType.CERTIFIER); + signatureAppearanceModel.setFontName(Objects.requireNonNull(fontNameBox.getSelectedItem()).toString()); + signatureAppearanceModel.setFontSize((int) ((ValueLabelItem) Objects.requireNonNull(fontSizeBox.getSelectedItem())).getValue()); + signatureAppearanceModel.setSignatureImagePath(imagePathTextField.getText()); + signatureAppearanceModel.setImageScale(imageScaleSlider.getValue()); + setSignatureImage(); + } + + + private void setSignatureImage() { + signatureAppearanceModel.setSignatureImagePath(imagePathTextField.getText()); + } + + private void buildAppearanceStream() { + + signatureWidgetAnnotation.resetAppearanceStream(new AffineTransform()); + signatureWidgetComponent.repaint(); + } + + private void setSelectedCertificate(X509Certificate certificate) { + signatureAppearanceModel.setSelectedCertificate(certificate != null); + if (certificate == null) { + // clear metadata + nameTextField.setText(""); + contactTextField.setText(""); + locationTextField.setText(""); + enableInputComponents(false); + signerHandler.setCertAlias(null); + } else { + // pull out metadata from cert. + X500Principal principal = certificate.getSubjectX500Principal(); + X500Name x500name = new X500Name(principal.getName()); + + enableInputComponents(true); + + if (x500name.getRDNs() != null) { + nameTextField.setText(SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.CN)); + contactTextField.setText(SignatureUtilities.parseRelativeDistinguishedName(x500name, + BCStyle.EmailAddress)); + String address = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.POSTAL_ADDRESS); + if (address != null) { + locationTextField.setText(SignatureUtilities.parseRelativeDistinguishedName(x500name, + BCStyle.POSTAL_ADDRESS)); + } else { + String state = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.ST); + if (state != null) { + locationTextField.setText(SignatureUtilities.parseRelativeDistinguishedName(x500name, + BCStyle.ST)); + } + } + + ArrayList location = new ArrayList<>(2); + String state = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.ST); + if (state != null) { + location.add(state); + } + String country = SignatureUtilities.parseRelativeDistinguishedName(x500name, BCStyle.C); + if (country != null) { + location.add(country); + } + if (!location.isEmpty()) { + locationTextField.setText(String.join(", ", location)); + } + } + } + updateModelAppearanceState(); + } + + private void buildUI() throws KeyStoreException { + + // need to build keystore right up front, so we can build out the JTable to show certs in the keychain + PasswordDialogCallbackHandler passwordDialogCallbackHandler = + new PasswordDialogCallbackHandler(this, messageBundle); + signerHandler = PkcsSignerFactory.getInstance(passwordDialogCallbackHandler); + + this.setTitle(messageBundle.getString("viewer.annotation.signature.creation.dialog.title")); + + JPanel certificateSelectionPanel = buildCertificateSelectionPanel(); + JPanel signatureBuilderPanel = buildSignatureBuilderPanel(); + JPanel signatureControlPanel = buildSignatureControlPanel(); + + JTabbedPane signatureTabbedPane = new JTabbedPane(); + signatureTabbedPane.addTab( + messageBundle.getString("viewer.annotation.signature.creation.dialog.certificate.tab.title"), + certificateSelectionPanel); + signatureTabbedPane.addTab( + messageBundle.getString("viewer.annotation.signature.creation.dialog.signature.tab.title"), + signatureBuilderPanel); + + enableInputComponents(false); + + JPanel contentPane = new JPanel(new BorderLayout()); + contentPane.add(signatureTabbedPane, BorderLayout.CENTER); + contentPane.add(signatureControlPanel, BorderLayout.SOUTH); + + // pack it up and go. + getContentPane().add(contentPane); + pack(); + setLocationRelativeTo(getOwner()); + setResizable(true); + } + + private JPanel buildSignatureBuilderPanel() { + JPanel appearancePanel = new JPanel(new GridBagLayout()); + appearancePanel.setAlignmentY(JPanel.TOP_ALIGNMENT); + + constraints = new GridBagConstraints(); + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.weightx = 1.0; + constraints.weighty = 0; + constraints.insets = new Insets(2, 10, 2, 10); + + JPanel visibilityPanel = new JPanel(new GridBagLayout()); + visibilityPanel.setAlignmentY(JPanel.TOP_ALIGNMENT); + visibilityPanel.setBorder(new TitledBorder(new EtchedBorder(EtchedBorder.LOWERED), + messageBundle.getString("viewer.annotation.signature.creation.dialog.signature.appearance.title"))); + // font name setup + String fontName = signatureAppearanceModel.getFontName(); + ValueLabelItem[] fontNameItems = FontWidgetUtilities.generateFontNameList(messageBundle); + fontNameBox = new JComboBox<>(fontNameItems); + fontNameBox.setSelectedItem(Arrays.stream(fontNameItems).filter(t -> t.getValue() == fontName).findAny().orElse(fontNameItems[0])); + fontNameBox.addItemListener(this); + + // font size setup + int fontSize = signatureAppearanceModel.getFontSize(); + ValueLabelItem[] fontSizeItems = FontWidgetUtilities.generateFontSizeNameList(messageBundle); + fontSizeBox = new JComboBox<>(fontSizeItems); + fontSizeBox.setSelectedItem(Arrays.stream(fontSizeItems).filter(t -> (int) t.getValue() == fontSize).findAny().orElse(fontSizeItems[0])); + fontSizeBox.addItemListener(this); + + // show text on signature appearance stream + boolean showText = signatureAppearanceModel.isSignatureTextVisible(); + showTextCheckBox = new JCheckBox(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.appearance.showText.label"), showText); + showTextCheckBox.addActionListener(this); + + // show image on signature appearance stream + boolean showImage = signatureAppearanceModel.isSignatureImageVisible(); + showSignatureCheckBox = new JCheckBox(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.appearance.showSignature.label"), showImage); + showSignatureCheckBox.addActionListener(this); + + // image path + JLabel imagePathLabel = new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.imagePath.label")); + imagePathTextField = new JTextField(); + String imagePath = signatureAppearanceModel.getSignatureImagePath(); + + imagePathTextField.setText(imagePath); + if (!imagePath.isEmpty()) { + signatureAppearanceModel.setSignatureImagePath(imagePath); + } + imagePathTextField.addFocusListener(this); + imagePathBrowseButton = new JButton(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.selection.browse.label")); + imagePathBrowseButton.addActionListener(this); + + // image scale + int imageScale = signatureAppearanceModel.getImageScale(); + JLabel imageScaleLabel = new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.imageScale.label")); + imageScaleSlider = new JSlider(JSlider.HORIZONTAL, 0, 300, imageScale); + imageScaleSlider.setMajorTickSpacing(50); + imageScaleSlider.setPaintLabels(true); + + imageScaleSlider.setPaintTicks(true); + imageScaleSlider.addChangeListener(this); + + // font name and size + addGB(visibilityPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.appearance.font.label")), + 0, 0, 1, 1); + addGB(visibilityPanel, fontNameBox, 1, 0, 1, 1); + addGB(visibilityPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.signature.appearance.fontSize.label")), + 2, 0, 1, 1); + addGB(visibilityPanel, fontSizeBox, 3, 0, 1, 1); + addGB(visibilityPanel, showTextCheckBox, 0, 1, 1, 2); + addGB(visibilityPanel, showSignatureCheckBox, 2, 1, 1, 2); + + JPanel signaturePanel = new JPanel(new GridBagLayout()); + signaturePanel.setAlignmentY(JPanel.TOP_ALIGNMENT); + signaturePanel.setBorder(new TitledBorder(new EtchedBorder(EtchedBorder.LOWERED), + messageBundle.getString("viewer.annotation.signature.creation.dialog.signature.canvas.title"))); + // image path input + addGB(signaturePanel, imagePathLabel, 0, 0, 1, 1); + addGB(signaturePanel, imagePathTextField, 1, 0, 1, 1); + addGB(signaturePanel, imagePathBrowseButton, 2, 0, 1, 1); + addGB(signaturePanel, imageScaleLabel, 0, 1, 1, 1); + addGB(signaturePanel, imageScaleSlider, 1, 1, 1, 2); + + constraints.insets = new Insets(2, 10, 2, 10); + addGB(appearancePanel, visibilityPanel, 0, 0, 1, 1); + addGB(appearancePanel, signaturePanel, 0, 1, 1, 1); + constraints.weighty = 1.0; + addGB(appearancePanel, new Label(" "), 0, 9, 1, 1); + + return appearancePanel; + } + + private JPanel buildSignatureControlPanel() { + JPanel controlPanel = new JPanel(new GridBagLayout()); + controlPanel.setAlignmentY(JPanel.BOTTOM_ALIGNMENT); + + // close buttons. + closeButton = new JButton(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.close.button.label")); + closeButton.setMnemonic(messageBundle.getString("viewer.button.cancel.mnemonic").charAt(0)); + closeButton.addActionListener(this); + signButton = new JButton(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.sign.button.label")); + signButton.addActionListener(this); + + constraints = new GridBagConstraints(); + constraints.fill = GridBagConstraints.NONE; + constraints.weightx = 1.0; + constraints.weighty = 0; + constraints.insets = new Insets(5, 10, 5, 10); + + // close and sign input + constraints.anchor = GridBagConstraints.WEST; + addGB(controlPanel, closeButton, 0, 0, 1, 1); + + constraints.anchor = GridBagConstraints.EAST; + addGB(controlPanel, signButton, 1, 0, 1, 1); + return controlPanel; + } + + private JPanel buildCertificateSelectionPanel() throws KeyStoreException { + JPanel certificateSelectionPanel = new JPanel(new GridBagLayout()); + certificateSelectionPanel.setAlignmentY(JPanel.TOP_ALIGNMENT); + this.setLayout(new BorderLayout()); + add(certificateSelectionPanel, BorderLayout.NORTH); + + // keystore certificate table + if (signerHandler == null) { + throw new IllegalStateException("Signer handler is null"); + } + Enumeration aliases = signerHandler.buildKeyStore().aliases(); + CertificateTableModel certificateTableModel = new CertificateTableModel(signerHandler, aliases, messageBundle); + certificateTable = new JTable(certificateTableModel); + certificateTable.getSelectionModel().addListSelectionListener(this); + certificateTable.setPreferredScrollableViewportSize(new Dimension(600, 100)); + certificateTable.setFillsViewportHeight(true); + + // certificate type selection + signerRadioButton = new JRadioButton(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.type.signer.label")); + signerRadioButton.setSelected(true); + signerRadioButton.addActionListener(this); + signerRadioButton.setActionCommand(SignatureType.SIGNER.toString()); + certifyRadioButton = new JRadioButton(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.type.certify.label")); + certifyRadioButton.addActionListener(this); + signerRadioButton.setActionCommand(SignatureType.CERTIFIER.toString()); + ButtonGroup certificateTypeButtonGroup = new ButtonGroup(); + certificateTypeButtonGroup.add(signerRadioButton); + certificateTypeButtonGroup.add(certifyRadioButton); + + // signature visibility + signerVisibilityCheckBox = new JCheckBox(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.visibility.certify.label")); + signerVisibilityCheckBox.setSelected(true); + signerVisibilityCheckBox.addActionListener(this); + + // location and date + locationTextField = new JTextField(); + locationTextField.addFocusListener(this); + JTextField dateTextField = new JTextField(); + dateTextField.setEnabled(false); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + String today = df.format(new Date()); + dateTextField.setText(today); + // name + nameTextField = new JTextField(); + nameTextField.addFocusListener(this); + // contact + contactTextField = new JTextField(); + contactTextField.addFocusListener(this); + + // todo very much needed -> Timestamp service + + // language + languagesComboBox = new JComboBox<>(supportedLocales); + // be nice to do this and take into account country too. + Locale defaultLocal = new Locale(Locale.getDefault().getLanguage()); + languagesComboBox.setSelectedItem(new Locale(Locale.getDefault().getLanguage())); + languagesComboBox.addItemListener(this); + signatureAppearanceModel.setLocale(defaultLocal); + + + constraints = new GridBagConstraints(); + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.weightx = 1.0; + constraints.weighty = 0; + constraints.insets = new Insets(2, 10, 2, 10); + + // cert selection label + addGB(certificateSelectionPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.selection.label")), + 0, 0, 1, 3); + + // cert table + addGB(certificateSelectionPanel, new JScrollPane(certificateTable), 0, 1, 1, 4); + + // type of signature. + addGB(certificateSelectionPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.type.description.label")), + 0, 2, 1, 1); + addGB(certificateSelectionPanel, signerRadioButton, 1, 2, 1, 1); + addGB(certificateSelectionPanel, certifyRadioButton, 2, 2, 1, 1); + // signature visibility + addGB(certificateSelectionPanel, signerVisibilityCheckBox, 3, 2, 1, 1); + + // location and Date + addGB(certificateSelectionPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.location.date.label")), + 0, 3, 1, 1); + addGB(certificateSelectionPanel, locationTextField, 1, 3, 1, 1); + addGB(certificateSelectionPanel, dateTextField, 2, 3, 1, 1); + + // name + addGB(certificateSelectionPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.name.label")), + 0, 5, 1, 1); + addGB(certificateSelectionPanel, nameTextField, 1, 5, 1, 3); + + // contact + addGB(certificateSelectionPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.contact.label")), + 0, 6, 1, 1); + addGB(certificateSelectionPanel, contactTextField, 1, 6, 1, 3); + + // language selection + constraints.anchor = GridBagConstraints.LINE_START; + addGB(certificateSelectionPanel, new JLabel(messageBundle.getString( + "viewer.annotation.signature.creation.dialog.certificate.i18n.label")), + 0, 8, 1, 1); + addGB(certificateSelectionPanel, languagesComboBox, 1, 8, 1, 1); + + constraints.weighty = 1.0; + addGB(certificateSelectionPanel, new Label(" "), 0, 9, 1, 1); + + return certificateSelectionPanel; + } + + private void enableInputComponents(boolean enable) { + nameTextField.setEnabled(enable); + contactTextField.setEnabled(enable); + locationTextField.setEnabled(enable); + signerRadioButton.setEnabled(enable); + certifyRadioButton.setEnabled(enable); + signerVisibilityCheckBox.setEnabled(enable); + languagesComboBox.setEnabled(enable); + + fontNameBox.setEnabled(enable); + fontSizeBox.setEnabled(enable); + showTextCheckBox.setEnabled(enable); + showSignatureCheckBox.setEnabled(enable); + imagePathTextField.setEnabled(enable); + + signButton.setEnabled(enable); + } + + private void addGB(JPanel layout, Component component, + int x, int y, + int rowSpan, int colSpan) { + constraints.gridx = x; + constraints.gridy = y; + constraints.gridwidth = colSpan; + constraints.gridheight = rowSpan; + layout.add(component, constraints); + } +} diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/summary/AnnotationSummaryPanel.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/summary/AnnotationSummaryPanel.java index babad83b3..24a5cf45a 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/summary/AnnotationSummaryPanel.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/common/views/annotations/summary/AnnotationSummaryPanel.java @@ -20,7 +20,7 @@ import org.icepdf.core.pobjects.annotations.MarkupAnnotation; import org.icepdf.core.util.PropertyConstants; import org.icepdf.ri.common.MutableDocument; -import org.icepdf.ri.common.utility.annotation.properties.FreeTextAnnotationPanel; +import org.icepdf.ri.common.utility.annotation.properties.FontWidgetUtilities; import org.icepdf.ri.common.utility.annotation.properties.ValueLabelItem; import org.icepdf.ri.common.views.Controller; import org.icepdf.ri.common.views.DocumentViewControllerImpl; @@ -200,7 +200,7 @@ protected void buildStatusToolBarPanel() { ViewerPropertiesManager propertiesManager = controller.getPropertiesManager(); - fontSizeBox = new JComboBox<>(FreeTextAnnotationPanel.generateFontSizeNameList(messageBundle)); + fontSizeBox = new JComboBox<>(FontWidgetUtilities.generateFontSizeNameList(messageBundle)); applySelectedValue(fontSizeBox, propertiesManager.checkAndStoreIntProperty( ViewerPropertiesManager.PROPERTY_ANNOTATION_SUMMARY_FONT_SIZE, new JLabel().getFont().getSize())); fontSizeBox.addItemListener(this); diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/util/ViewerPropertiesManager.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/util/ViewerPropertiesManager.java index 09a3e0c82..ff205cfe6 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/util/ViewerPropertiesManager.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/util/ViewerPropertiesManager.java @@ -89,6 +89,18 @@ public final class ViewerPropertiesManager { public static final String PROPERTY_PAGE_VIEW_BACKGROUND_COLOR = "org.icepdf.core.views.background.color"; // image reference type. public static final String PROPERTY_IMAGING_REFERENCE_TYPE = "org.icepdf.core.imageReference"; + // signature creation keystore type handler. + public static final String PROPERTY_PKCS_KEYSTORE_TYPE = "org.icepdf.core.signatures.keystore.type"; + public static final String PROPERTY_PKCS11_PROVIDER_CONFIG_PATH = "org.icepdf.core.signatures.keystore.pkcs11" + + ".config.path"; + public static final String PROPERTY_PKCS12_PROVIDER_KEYSTORE_PATH = "org.icepdf.core.signatures.keystore.pkcs12" + + ".config.path"; + public static final String PROPERTY_SIGNATURE_IMAGE_PATH = "org.icepdf.core.signatures.image.path"; + public static final String PROPERTY_SIGNATURE_SHOW_TEXT = "org.icepdf.core.signatures.show.txt"; + public static final String PROPERTY_SIGNATURE_SHOW_IMAGE = "org.icepdf.core.signatures.show.image"; + public static final String PROPERTY_SIGNATURE_IMAGE_SCALE = "org.icepdf.core.signatures.show.imageScale"; + public static final String PROPERTY_SIGNATURE_FONT_NAME = "org.icepdf.core.signatures.font.name"; + public static final String PROPERTY_SIGNATURE_FONT_SIZE = "org.icepdf.core.signatures.font.size"; // advanced threading properties public static final String PROPERTY_IMAGE_PROXY_ENABLED = "org.icepdf.core.imageProxy"; public static final String PROPERTY_IMAGE_PROXY_THREAD_COUNT = "org.icepdf.core.library.imageThreadPoolSize"; @@ -138,6 +150,7 @@ public final class ViewerPropertiesManager { public static final String PROPERTY_SHOW_PREFERENCES_GENERAL = "application.preferences.show.general"; public static final String PROPERTY_SHOW_PREFERENCES_ANNOTATIONS = "application.preferences.show.annotations"; public static final String PROPERTY_SHOW_PREFERENCES_IMAGING = "application.preferences.show.imaging"; + public static final String PROPERTY_SHOW_PREFERENCES_SIGNING = "application.preferences.show.signing"; public static final String PROPERTY_SHOW_PREFERENCES_FONTS = "application.preferences.show.fonts"; public static final String PROPERTY_SHOW_PREFERENCES_ADVANCED = "application.preferences.show.advanced"; public static final String PROPERTY_SHOW_PREFERENCES_EXIMPORT = "application.preferences.show.eximport"; @@ -161,6 +174,8 @@ public final class ViewerPropertiesManager { ".selection.type"; public static final String PROPERTY_ANNOTATION_LINE_SELECTION_TYPE = "application.annotation.line.selection.type"; public static final String PROPERTY_ANNOTATION_LINK_SELECTION_TYPE = "application.annotation.link.selection.type"; + public static final String PROPERTY_ANNOTATION_SIGNATURE_SELECTION_TYPE = "application.annotation.signature" + + ".selection.type"; public static final String PROPERTY_ANNOTATION_SQUARE_SELECTION_TYPE = "application.annotation.rectangle.selection.type"; public static final String PROPERTY_ANNOTATION_CIRCLE_SELECTION_TYPE = "application.annotation.circle.selection.type"; public static final String PROPERTY_ANNOTATION_INK_SELECTION_TYPE = "application.annotation.ink.selection.type"; @@ -172,6 +187,7 @@ public final class ViewerPropertiesManager { ViewerPropertiesManager.PROPERTY_ANNOTATION_INK_SELECTION_TYPE, ViewerPropertiesManager.PROPERTY_ANNOTATION_LINE_SELECTION_TYPE, ViewerPropertiesManager.PROPERTY_ANNOTATION_LINK_SELECTION_TYPE, + ViewerPropertiesManager.PROPERTY_ANNOTATION_SIGNATURE_SELECTION_TYPE, ViewerPropertiesManager.PROPERTY_ANNOTATION_SQUARE_SELECTION_TYPE, ViewerPropertiesManager.PROPERTY_ANNOTATION_TEXT_SELECTION_TYPE, ViewerPropertiesManager.PROPERTY_ANNOTATION_FREE_TEXT_SELECTION_TYPE @@ -195,6 +211,8 @@ public final class ViewerPropertiesManager { public static final String PROPERTY_SHOW_TOOLBAR_ANNOTATION_HIGHLIGHT = "application.toolbar.annotation.highlight.enabled"; public static final String PROPERTY_SHOW_TOOLBAR_ANNOTATION_REDACTION = "application.toolbar.annotation.redaction" + ".enabled"; + public static final String PROPERTY_SHOW_TOOLBAR_ANNOTATION_SIGNATURE = "application.toolbar.annotation.signature" + + ".enabled"; public static final String PROPERTY_SHOW_TOOLBAR_ANNOTATION_UNDERLINE = "application.toolbar.annotation.underline.enabled"; public static final String PROPERTY_SHOW_TOOLBAR_ANNOTATION_STRIKE_OUT = "application.toolbar.annotation.strikeout.enabled"; public static final String PROPERTY_SHOW_TOOLBAR_ANNOTATION_LINE = "application.toolbar.annotation.line.enabled"; diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/Launcher.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/Launcher.java index f7cd973ca..3ac602a66 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/Launcher.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/Launcher.java @@ -29,6 +29,9 @@ import java.util.logging.Logger; import java.util.prefs.Preferences; +import static org.icepdf.ri.common.preferences.SigningPreferencesPanel.PKCS_11_TYPE; +import static org.icepdf.ri.common.preferences.SigningPreferencesPanel.PKCS_12_TYPE; + /** *

Launches the Viewer Application. The following parameters can be used * to optionally load a PDF document at startup.

@@ -49,6 +52,16 @@ * URL. Use the following syntax:
* -loadurl http://www.examplesite.com/file.pdf * + * + * -pkcs11path filename + * Specifies the location of the PKCS#11 provider configuration file to use when signing a document:
+ * -pkcs11path /home/user/myProvider.cfg + * + * + * -pkcs12path filename + * Specifies the location of the PKCS#12 keystore file that will be used when signing a document:
+ * -pkcs12path /home/user/certificate.pfx" + * * */ public class Launcher { @@ -67,6 +80,8 @@ public static void main(String[] argv) { String contentURL = ""; String contentFile = ""; + String pkcs11ConfigPath = ""; + String pkcs12KeystorePath = ""; String printer = null; // parse command line arguments for (int i = 0; i < argv.length; i++) { @@ -82,6 +97,12 @@ public static void main(String[] argv) { case "-loadurl": contentURL = argv[++i].trim(); break; + case "-pkcs11path": + pkcs11ConfigPath = argv[++i].trim(); + break; + case "-pkcs12path": + pkcs12KeystorePath = argv[++i].trim(); + break; case "-print": printer = argv[++i].trim(); break; @@ -95,13 +116,13 @@ public static void main(String[] argv) { ResourceBundle messageBundle = ResourceBundle.getBundle( ViewerPropertiesManager.DEFAULT_MESSAGE_BUNDLE); - // Quit if there where any problems parsing the command line arguments + // Quit if there were any problems parsing the command line arguments if (brokenUsage) { System.out.println(messageBundle.getString("viewer.commandLin.error")); System.exit(1); } // start the viewer - run(contentFile, contentURL, printer, messageBundle); + run(contentFile, contentURL, printer, pkcs11ConfigPath, pkcs12KeystorePath, messageBundle); } /** @@ -112,11 +133,15 @@ public static void main(String[] argv) { * @param contentURL URL of a file which will be loaded at runtime, can be * null. * @param printer The name of the printer to use, can be null + * @param pkcs11ConfigPath set path of a PKCS#11 config file to that will be used by the singer UI. + * @param pkcs12KeystorePath set path if a PKCS#12 keystore file that will be used by the signer UI * @param messageBundle messageBundle to pull strings from */ private static void run(String contentFile, String contentURL, String printer, + String pkcs11ConfigPath, + String pkcs12KeystorePath, ResourceBundle messageBundle) { // initiate the properties manager. @@ -133,6 +158,17 @@ private static void run(String contentFile, ViewModel.setDefaultURL(propertiesManager.getPreferences().get( ViewerPropertiesManager.PROPERTY_DEFAULT_URL, null)); + // override any current PKCS paths with command line overrides. + if (pkcs12KeystorePath != null && !pkcs12KeystorePath.isEmpty()) { + propertiesManager.getPreferences().put(ViewerPropertiesManager.PROPERTY_PKCS12_PROVIDER_KEYSTORE_PATH, + pkcs12KeystorePath); + propertiesManager.getPreferences().put(ViewerPropertiesManager.PROPERTY_PKCS_KEYSTORE_TYPE, PKCS_12_TYPE); + } else if (pkcs11ConfigPath != null && !pkcs11ConfigPath.isEmpty()) { + propertiesManager.getPreferences().put(ViewerPropertiesManager.PROPERTY_PKCS11_PROVIDER_CONFIG_PATH, + pkcs11ConfigPath); + propertiesManager.getPreferences().put(ViewerPropertiesManager.PROPERTY_PKCS_KEYSTORE_TYPE, PKCS_11_TYPE); + } + // application instance WindowManager windowManager = WindowManager.createInstance(propertiesManager, messageBundle); if (contentFile != null && !contentFile.isEmpty()) { diff --git a/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/WindowManager.java b/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/WindowManager.java index 8d7a90252..1c8b21285 100644 --- a/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/WindowManager.java +++ b/viewer/viewer-awt/src/main/java/org/icepdf/ri/viewer/WindowManager.java @@ -22,6 +22,7 @@ import org.icepdf.ri.common.views.Controller; import org.icepdf.ri.common.views.DocumentViewController; import org.icepdf.ri.common.views.DocumentViewControllerImpl; +import org.icepdf.ri.common.views.annotations.signing.BasicSignatureAppearanceCallback; import org.icepdf.ri.util.ViewerPropertiesManager; import javax.swing.*; @@ -152,6 +153,8 @@ protected Controller commonWindowCreation(boolean isVisible) { // add interactive mouse link annotation support controller.getDocumentViewController().setAnnotationCallback( new MyAnnotationCallback(controller.getDocumentViewController())); + // add custom signature appearance stream callback + controller.getDocumentViewController().setSignatureAppearanceCallback(new BasicSignatureAppearanceCallback()); controllers.add(controller); // guild a new swing viewer with remembered view settings. diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_a_24.png b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_a_24.png new file mode 100644 index 000000000..c58cb27fb Binary files /dev/null and b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_a_24.png differ diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_a_32.png b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_a_32.png new file mode 100644 index 000000000..74d954e64 Binary files /dev/null and b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_a_32.png differ diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_i_24.png b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_i_24.png new file mode 100644 index 000000000..336283729 Binary files /dev/null and b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_i_24.png differ diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_i_32.png b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_i_32.png new file mode 100644 index 000000000..cb43e0e41 Binary files /dev/null and b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_i_32.png differ diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_r_24.png b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_r_24.png new file mode 100644 index 000000000..0f65ba53a Binary files /dev/null and b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_r_24.png differ diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_r_32.png b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_r_32.png new file mode 100644 index 000000000..0795c4d1c Binary files /dev/null and b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/images/signature_annot_r_32.png differ diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/resources/MessageBundle.properties b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/resources/MessageBundle.properties index 3a0ae1a9d..6a7072093 100644 --- a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/resources/MessageBundle.properties +++ b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/resources/MessageBundle.properties @@ -117,6 +117,8 @@ viewer.toolbar.tool.highlight.label=Highlight viewer.toolbar.tool.highlight.tooltip=Highlight Annotation Tool viewer.toolbar.tool.redaction.label=Redaction viewer.toolbar.tool.redaction.tooltip=Redaction Annotation Tool +viewer.toolbar.tool.signature.label=Signature +viewer.toolbar.tool.signature.tooltip=Signature Annotation Tool viewer.toolbar.tool.strikeOut.label=Strike Out viewer.toolbar.tool.strikeOut.tooltip=Strike Out Annotation Tool viewer.toolbar.tool.underline.label=Underline @@ -284,6 +286,7 @@ viewer.dialog.viewerPreferences.title=Viewer Preferences viewer.dialog.viewerPreferences.section.general.title=General viewer.dialog.viewerPreferences.section.annotations.title=Annotations viewer.dialog.viewerPreferences.section.imaging.title=Imaging +viewer.dialog.viewerPreferences.section.signatures.title=Signatures viewer.dialog.viewerPreferences.section.fonts.title=Fonts viewer.dialog.viewerPreferences.section.advanced.title=Advanced viewer.dialog.viewerPreferences.section.eximport.title=Export/Import @@ -319,6 +322,16 @@ viewer.dialog.viewerPreferences.section.imaging.imageReference.scaled.label=Scal viewer.dialog.viewerPreferences.section.imaging.imageReference.mipMap.label=MIP Map viewer.dialog.viewerPreferences.section.imaging.imageReference.smothScaled.label=Smooth scaled viewer.dialog.viewerPreferences.section.imaging.imageReference.blurred.label=Blurred +## Signatures preferences dialog +viewer.dialog.viewerPreferences.section.signatures.pkcs.border.label=PKCS Settings +viewer.dialog.viewerPreferences.section.signatures.pkcs.label=PKCS Format: +viewer.dialog.viewerPreferences.section.signatures.pkcs.11.label=PKCS#11 +viewer.dialog.viewerPreferences.section.signatures.pkcs.12.label=PKCS#12 +viewer.dialog.viewerPreferences.section.signatures.pkcs.11.config.path.label=PKCS#11 Config File Path: +viewer.dialog.viewerPreferences.section.signatures.pkcs.12.keystore.path.label=PKCS#12 File Path: +viewer.dialog.viewerPreferences.section.signatures.pkcs.keystore.path.selection.title=Select a keystore file +viewer.dialog.viewerPreferences.section.signatures.pkcs.keystore.path.accept.label=Select +viewer.dialog.viewerPreferences.section.signatures.pkcs.keystore.path.browse.label=Browse ## fonts preferences dialog viewer.dialog.viewerPreferences.section.fonts.fontCache.border.label=Font Cache viewer.dialog.viewerPreferences.section.fonts.fontCache.label=Reset Font Cache: @@ -425,6 +438,10 @@ viewer.dialog.redaction.unburned.title=Unburned Redaction Annotation Detected viewer.dialog.redaction.unburned.msgs=\ The PDF document contains Redaction Annotations that have not yet been burned into the document. \ \nWould you like to export the document instead of saving? +## Signature "multiple" signatures warning messages +viewer.dialog.signature.creation.title=Signature Creation Warning +viewer.dialog.signature.creation.msgs=\ + The PDF document already contains a signature, document must be saved before adding another. ## Export Text Dialog viewer.dialog.exportText.title=Export Document Text viewer.dialog.exportText.progress.msg=Extracting PDF Text @@ -868,6 +885,8 @@ viewer.annotation.popup.addAnnotation.strikeout.label=Add Strikeout viewer.annotation.popup.color.change.label=Change color... viewer.annotation.popup.text.extract.label=Copy highlighted text ## Signature component +viewer.annotation.signature.menu.addSignature.label=Add Signature +viewer.annotation.signature.menu.deleteSignature.label=Delete Signature viewer.annotation.signature.menu.validateSignature.label=Validate Signature viewer.annotation.signature.menu.showCertificates.label=Show Certificate Properties viewer.annotation.signature.menu.signatureProperties.label=Show Signature Properties @@ -912,6 +931,60 @@ viewer.annotation.signature.properties.dialog.certificateExpired.failure=- Signe viewer.annotation.signature.properties.dialog.showCertificates.label=Signer's Certificate... viewer.annotation.signature.properties.dialog.validity.title=Validity Summary viewer.annotation.signature.properties.dialog.signerInfo.title=Signer Info +# keystore password callback dialog +viewer.annotation.signature.creation.keystore.pkcs11.dialog.title=Keystore Pin +viewer.annotation.signature.creation.keystore.pkcs11.dialog.label=Pin: +viewer.annotation.signature.creation.keystore.pkcs12.dialog.title=Keystore Password +viewer.annotation.signature.creation.keystore.pkcs12.dialog.label=Password: +# Signature annotation creation dialog +viewer.annotation.signature.creation.dialog.title=Signature Creation +viewer.annotation.signature.creation.dialog.certificate.tab.title=Certificate +viewer.annotation.signature.creation.dialog.certificate.selection.label=Please choose the desired certificate +viewer.annotation.signature.creation.dialog.certificate.table.name.label=Name +viewer.annotation.signature.creation.dialog.certificate.table.author.label=Email +viewer.annotation.signature.creation.dialog.certificate.table.validity.label=Validity +viewer.annotation.signature.creation.dialog.certificate.table.description.label=Description +viewer.annotation.signature.creation.dialog.certificate.type.description.label=Type of Signature: +viewer.annotation.signature.creation.dialog.certificate.type.signer.label=Approval (multiple signatures) +viewer.annotation.signature.creation.dialog.certificate.type.certify.label=Certification +viewer.annotation.signature.creation.dialog.certificate.visibility.certify.label=Signature/certificate visible +viewer.annotation.signature.creation.dialog.certificate.location.date.label=Place of signature/Date: +viewer.annotation.signature.creation.dialog.certificate.name.label=Name: +viewer.annotation.signature.creation.dialog.certificate.contact.label=Contact: +viewer.annotation.signature.creation.dialog.certificate.reason.label=Reason: +viewer.annotation.signature.creation.dialog.certificate.timestamp.label=Insert Timestamp Token (TSA): +viewer.annotation.signature.creation.dialog.certificate.i18n.label=Language: +viewer.annotation.signature.creation.dialog.signature.tab.title=Signature +viewer.annotation.signature.creation.dialog.signature.appearance.title=Appearance +viewer.annotation.signature.creation.dialog.signature.appearance.font.label=Font: +viewer.annotation.signature.creation.dialog.signature.appearance.fontSize.label=Size: +viewer.annotation.signature.creation.dialog.signature.appearance.showText.label=Show Text +viewer.annotation.signature.creation.dialog.signature.appearance.showSignature.label=Show Signature +viewer.annotation.signature.creation.dialog.signature.canvas.title=Signature +viewer.annotation.signature.creation.dialog.signature.imagePath.label=Signature Image: +viewer.annotation.signature.creation.dialog.signature.selection.title=Select a signature image +viewer.annotation.signature.creation.dialog.signature.selection.accept.label=Select +viewer.annotation.signature.creation.dialog.signature.selection.browse.label=Browse +viewer.annotation.signature.creation.dialog.signature.imageScale.label=Scale: +viewer.annotation.signature.creation.dialog.close.button.label=Close +viewer.annotation.signature.creation.dialog.sign.button.label=Add Signature +# multiple certifier error dialog +viewer.annotation.signature.creation.dialog.certify.error.title=Signature Error +viewer.annotation.signature.creation.dialog.certify.error.msg=Certifier signature already exists +# keystore authentication error dialog +viewer.annotation.signature.authentication.failure.dialog.certify.error.title=Authentication Error +viewer.annotation.signature.authentication.failure.dialog.certify.error.msg=Keystore authentication failed +# keystore not configured error dialog +viewer.annotation.signature.keystore.failure.dialog.certify.error.title=Authentication Error +viewer.annotation.signature.keystore.failure.dialog.certify.error.msg=\ + Keystore has not been configured, please configure keystore in the preferences dialog. +# Signature annotation creation handler +viewer.annotation.signature.handler.properties.reason.label=Reason: {0} +viewer.annotation.signature.handler.properties.reason.approval.label=Approval +viewer.annotation.signature.handler.properties.reason.certification.label=Certification +viewer.annotation.signature.handler.properties.contact.label=Contact: {0} +viewer.annotation.signature.handler.properties.signer.label=Digitally signed by {0} +viewer.annotation.signature.handler.properties.location.label={0} ## Common Button Labels viewer.button.ok.label=Ok viewer.button.ok.mnemonic=O diff --git a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/viewer/res/ICEpdfDefault.properties b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/viewer/res/ICEpdfDefault.properties index bb7eef051..f2c5daad4 100644 --- a/viewer/viewer-awt/src/main/resources/org/icepdf/ri/viewer/res/ICEpdfDefault.properties +++ b/viewer/viewer-awt/src/main/resources/org/icepdf/ri/viewer/res/ICEpdfDefault.properties @@ -36,6 +36,7 @@ document.fixedfont.size = 11 application.toolbar.annotation.selection.enabled=true application.toolbar.annotation.highlight.enabled=true application.toolbar.annotation.redaction.enabled=true +application.toolbar.annotation.signature.enabled=true application.toolbar.annotation.underline.enabled=false application.toolbar.annotation.strikeout.enabled=false application.toolbar.annotation.line.enabled=false @@ -47,6 +48,9 @@ application.toolbar.annotation.ink.enabled=false application.toolbar.annotation.freetext.enabled=false application.toolbar.annotation.text.enabled=true application.toolbar.annotation.permission.enabled=false +application.toolbar.annotation.delete.enabled=false +application.toolbar.bookmark.toolbar.enabled=false +application.toolbar.annotation.preview.enabled=false # annotations that default selection tool after creation application.annotation.highlight.selection.type=0 application.annotation.line.selection.type=0 @@ -56,6 +60,7 @@ application.annotation.circle.selection.type=0 application.annotation.ink.selection.type=0 application.annotation.freetext.selection.type=0 application.annotation.text.selection.type=0 +application.annotation.signature.selection.type=0 # markup annotation (comments)utility pane settings application.viewer.utility.annotation.sort.column=PAGE application.viewer.utility.annotation.filter.type.column=ALL diff --git a/viewer/viewer-awt/src/test/java/org/icepdf/core/pobjects/acroform/SigningTests.java b/viewer/viewer-awt/src/test/java/org/icepdf/core/pobjects/acroform/SigningTests.java new file mode 100644 index 000000000..9a487ced5 --- /dev/null +++ b/viewer/viewer-awt/src/test/java/org/icepdf/core/pobjects/acroform/SigningTests.java @@ -0,0 +1,127 @@ +package org.icepdf.core.pobjects.acroform; + +import org.icepdf.core.pobjects.Document; +import org.icepdf.core.pobjects.PDate; +import org.icepdf.core.pobjects.acroform.signature.appearance.SignatureType; +import org.icepdf.core.pobjects.acroform.signature.handlers.Pkcs12SignerHandler; +import org.icepdf.core.pobjects.acroform.signature.handlers.SimplePasswordCallbackHandler; +import org.icepdf.core.pobjects.acroform.signature.utils.SignatureUtilities; +import org.icepdf.core.pobjects.annotations.AnnotationFactory; +import org.icepdf.core.pobjects.annotations.SignatureWidgetAnnotation; +import org.icepdf.core.util.Library; +import org.icepdf.core.util.SignatureManager; +import org.icepdf.core.util.updater.WriteMode; +import org.icepdf.ri.common.views.annotations.signing.BasicSignatureAppearanceCallback; +import org.icepdf.ri.common.views.annotations.signing.SignatureAppearanceModelImpl; +import org.icepdf.ri.util.FontPropertiesManager; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.Date; +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.fail; + +public class SigningTests { + + @BeforeAll + public static void init() { + FontPropertiesManager.getInstance().loadOrReadSystemFonts(); + } + + @DisplayName("signatures - should create signed document") + @Test + public void testXrefTableFullUpdate() { + + try { + String keystorePath = "/home/pcorless/dev/cert-test/openssl-keypair/certificate.pfx"; + String password = "changeit"; + String certAlias = "senderKeyPair"; + + Pkcs12SignerHandler pkcs12SignerHandler = new Pkcs12SignerHandler(new File(keystorePath), certAlias, + new SimplePasswordCallbackHandler(password)); + + Document document = new Document(); + InputStream fileUrl = SigningTests.class.getResourceAsStream("/signing/test_print.pdf"); + document.setInputStream(fileUrl, "test_print.pdf"); + Library library = document.getCatalog().getLibrary(); + SignatureManager signatureManager = library.getSignatureDictionaries(); + + // Create signature annotation + SignatureWidgetAnnotation signatureAnnotation = + (SignatureWidgetAnnotation) AnnotationFactory.buildWidgetAnnotation( + document.getPageTree().getLibrary(), + FieldDictionaryFactory.TYPE_SIGNATURE, + new Rectangle(100, 250, 375, 150)); + document.getPageTree().getPage(0).addAnnotation(signatureAnnotation, true); + + // Add the signatureWidget to catalog + InteractiveForm interactiveForm = document.getCatalog().getOrCreateInteractiveForm(); + interactiveForm.addField(signatureAnnotation); + + // set up signer dictionary as the primary certification signer. + SignatureDictionary signatureDictionary = + SignatureDictionary.getInstance(signatureAnnotation, SignatureType.CERTIFIER); + signatureDictionary.setSignerHandler(pkcs12SignerHandler); + signatureDictionary.setReason("Approval"); // Approval or certification but technically can be anything + signatureDictionary.setDate(PDate.formatDateTime(new Date())); + signatureManager.addSignature(signatureDictionary, signatureAnnotation); + + // assign cert metadata to dictionary + SignatureUtilities.updateSignatureDictionary(signatureDictionary, pkcs12SignerHandler.getCertificate()); + + // build basic appearance + SignatureAppearanceModelImpl signatureAppearanceModel = new SignatureAppearanceModelImpl(library); + signatureAppearanceModel.setLocale(Locale.ENGLISH); + signatureAppearanceModel.setName(signatureDictionary.getName()); + signatureAppearanceModel.setContact(signatureDictionary.getContactInfo()); + signatureAppearanceModel.setLocation(signatureDictionary.getLocation()); + signatureAppearanceModel.setSignatureType(signatureDictionary.getReason().equals("Approval") ? + SignatureType.SIGNER : SignatureType.CERTIFIER); + signatureAppearanceModel.setSignatureImage(createTestSignatureBufferedImage()); + + BasicSignatureAppearanceCallback signatureAppearance = new BasicSignatureAppearanceCallback(); + signatureAppearance.setSignatureAppearanceModel(signatureAppearanceModel); + signatureAnnotation.setAppearanceCallback(signatureAppearance); + signatureAnnotation.resetAppearanceStream(new AffineTransform()); + + // Most common workflow is to add just one signature as we do here + File out = new File("./src/test/out/SigningTest_signed_document.pdf"); + try (BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(out), 8192)) { + document.saveToOutputStream(stream, WriteMode.INCREMENT_UPDATE); + } + // open the signed document + Document modifiedDocument = new Document(); + modifiedDocument.setFile(out.getAbsolutePath()); + + } catch (Exception e) { + // make sure we have no io errors. + e.printStackTrace(); + fail("should not be any exceptions"); + } + } + + private BufferedImage createTestSignatureBufferedImage() { + BufferedImage image = new BufferedImage(150, 50, BufferedImage.TYPE_INT_ARGB); + Graphics2D imageGraphics = image.createGraphics(); + imageGraphics.setStroke(new BasicStroke(2)); + imageGraphics.setColor(new Color(255, 255, 255)); + imageGraphics.fillRect(0, 0, 150, 50); + imageGraphics.setColor(Color.BLUE); + imageGraphics.fillRect(0, 0, 100, 25); + imageGraphics.setColor(Color.RED); + imageGraphics.drawRect(0, 0, 100, 25); + imageGraphics.dispose(); + return image; + } + + +} diff --git a/viewer/viewer-awt/src/test/resources/signing/test_print.pdf b/viewer/viewer-awt/src/test/resources/signing/test_print.pdf new file mode 100644 index 000000000..d6d9bc33c Binary files /dev/null and b/viewer/viewer-awt/src/test/resources/signing/test_print.pdf differ