/*******************************************************************************
* Copyright 2014 by E-Government Innovation Center EGIZ, Graz, Austria
* PDF-AS has been contracted by the E-Government Innovation Center EGIZ, a
* joint initiative of the Federal Chancellery Austria and Graz University of
* Technology.
*
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved by
* the European Commission - subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at:
* http://www.osor.eu/eupl/
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and
* limitations under the Licence.
*
* This product combines work with different licenses. See the "NOTICE" text
* file for details on the various modules and licenses.
* The "NOTICE" text file is part of the distribution. Any derivative works
* that you distribute must include a readable copy of the "NOTICE" text file.
******************************************************************************/
package at.gv.egiz.pdfas.lib.impl.signing.pdfbox2;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.activation.DataSource;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDNumberTreeNode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement;
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.preflight.PreflightDocument;
import org.apache.pdfbox.preflight.ValidationResult;
import org.apache.pdfbox.preflight.exception.SyntaxValidationException;
import org.apache.pdfbox.preflight.exception.ValidationException;
import org.apache.pdfbox.preflight.parser.PreflightParser;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.schema.PDFAIdentificationSchema;
import org.apache.xmpbox.xml.DomXmpParser;
import at.gv.egiz.pdfas.common.exceptions.PDFASError;
import at.gv.egiz.pdfas.common.exceptions.PdfAsException;
import at.gv.egiz.pdfas.common.messages.MessageResolver;
import at.gv.egiz.pdfas.common.settings.SignatureProfileSettings;
import at.gv.egiz.pdfas.lib.api.ByteArrayDataSource;
import at.gv.egiz.pdfas.lib.api.IConfigurationConstants;
import at.gv.egiz.pdfas.lib.api.sign.IPlainSigner;
import at.gv.egiz.pdfas.lib.api.sign.SignParameter;
import at.gv.egiz.pdfas.lib.impl.ErrorExtractor;
import at.gv.egiz.pdfas.lib.impl.SignaturePositionImpl;
import at.gv.egiz.pdfas.lib.impl.configuration.SignatureProfileConfiguration;
import at.gv.egiz.pdfas.lib.impl.pdfbox2.PDFBOXObject;
import at.gv.egiz.pdfas.lib.impl.pdfbox2.positioning.Positioning;
import at.gv.egiz.pdfas.lib.impl.pdfbox2.utils.PdfBoxUtils;
import at.gv.egiz.pdfas.lib.impl.placeholder.PlaceholderFilter;
import at.gv.egiz.pdfas.lib.impl.placeholder.SignaturePlaceholderData;
import at.gv.egiz.pdfas.lib.impl.signing.IPdfSigner;
import at.gv.egiz.pdfas.lib.impl.signing.PDFASSignatureExtractor;
import at.gv.egiz.pdfas.lib.impl.signing.PDFASSignatureInterface;
import at.gv.egiz.pdfas.lib.impl.stamping.IPDFStamper;
import at.gv.egiz.pdfas.lib.impl.stamping.IPDFVisualObject;
import at.gv.egiz.pdfas.lib.impl.stamping.TableFactory;
import at.gv.egiz.pdfas.lib.impl.stamping.ValueResolver;
import at.gv.egiz.pdfas.lib.impl.stamping.pdfbox2.PDFAsVisualSignatureProperties;
import at.gv.egiz.pdfas.lib.impl.stamping.pdfbox2.PdfBoxVisualObject;
import at.gv.egiz.pdfas.lib.impl.stamping.pdfbox2.StamperFactory;
import at.gv.egiz.pdfas.lib.impl.status.OperationStatus;
import at.gv.egiz.pdfas.lib.impl.status.PDFObject;
import at.gv.egiz.pdfas.lib.impl.status.RequestedSignature;
import at.knowcenter.wag.egov.egiz.pdf.PositioningInstruction;
import at.knowcenter.wag.egov.egiz.pdf.TablePos;
import at.knowcenter.wag.egov.egiz.table.Table;
import iaik.x509.X509Certificate;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PADESPDFBOXSigner implements IPdfSigner, IConfigurationConstants {
@Override
public void signPDF(PDFObject genericPdfObject, RequestedSignature requestedSignature,
PDFASSignatureInterface genericSigner) throws PdfAsException {
boolean isAdobeSigForm = false;
if (!(genericPdfObject instanceof PDFBOXObject)) {
throw new PdfAsException("PDF to signObject is of wrong type: " + genericPdfObject.getClass().getName());
}
if (!(genericSigner instanceof PDFASPDFBOXSignatureInterface)) {
throw new PdfAsException("PDF signerObject is of wrong type:" + genericSigner.getClass().getName());
}
final PDFBOXObject pdfObject = (PDFBOXObject) genericPdfObject;
final PDFASPDFBOXSignatureInterface signer = (PDFASPDFBOXSignatureInterface) genericSigner;
PDDocument doc = null;
SignatureOptions options = new SignatureOptions();
try {
doc = pdfObject.getDocument();
// sign a PDF with an existing empty signature, as created by the
// CreateEmptySignatureForm example.
PDSignature signature = findExistingSignature(doc, getSignatureFieldNameConfig(pdfObject));
if (signature == null) {
// create signature dictionary
signature = new PDSignature();
} else {
isAdobeSigForm = true;
}
// set basic signature parameters
signature.setFilter(COSName.getPDFName(signer.getPDFFilter()));
signature.setSubFilter(COSName.getPDFName(signer.getPDFSubFilter()));
signature.setSignDate(Calendar.getInstance());
log.debug("Signing @ " + signature.getSignDate().getTime().toString());
// extract next QR-code placeholder, if exists
SignaturePlaceholderData nextPlaceholderData = PlaceholderFilter.checkPlaceholderSignatureLocation(
pdfObject.getStatus(), pdfObject.getStatus().getSettings(),
pdfObject.getStatus().getSignParamter().getPlaceHolderId());
if (nextPlaceholderData != null) {
log.info("Placeholder data found.");
signature.setLocation(nextPlaceholderData.getPlaceholderName());
if (nextPlaceholderData.getProfile() != null) {
if (pdfObject.getStatus().getSettings().isValue(IConfigurationConstants.PLACEHOLDER_PROFILE_OVERWRITE, true)) {
log.debug("Placeholder Profile set to: {}", nextPlaceholderData.getProfile());
requestedSignature.setSignatureProfileID(nextPlaceholderData.getProfile());
} else {
log.debug("Placeholder profile over-write is disabled. Using profile from request ... ");
}
}
}
final SignatureProfileSettings signatureProfileSettings =
TableFactory.createProfile(requestedSignature.getSignatureProfileID(), pdfObject.getStatus().getSettings());
// set signature name
final ValueResolver resolver = new ValueResolver(requestedSignature, pdfObject.getStatus());
final String signerName = resolver.resolve("SIG_SUBJECT",
signatureProfileSettings.getValue("SIG_SUBJECT"), signatureProfileSettings);
signature.setName(signerName);
// set signature reason
String signerReason = signatureProfileSettings.getSigningReason() != null
? signatureProfileSettings.getSigningReason() : "PAdES Signature";
signature.setReason(signerReason);
log.debug("Signing reason: " + signerReason);
signer.setPDSignature(signature);
if (signatureProfileSettings.isPDFA() || signatureProfileSettings.isPDFA3()) {
signatureProfileSettings.setPDFAVersion(getPDFAVersion(doc));
}
// prepare basic signature options
options.setPreferredSignatureSize(calculateBlankAreaForSignature(signatureProfileSettings));
PDFAsVisualSignatureProperties properties = null;
if (requestedSignature.isVisual()) {
log.info("Creating visual siganture block");
final SignatureProfileConfiguration signatureProfileConfiguration =
pdfObject.getStatus().getSignatureProfileConfiguration(requestedSignature.getSignatureProfileID());
final TablePos tablePos = prepareTablePosition(nextPlaceholderData, signatureProfileConfiguration,
pdfObject.getStatus().getSignParamter().getSignaturePosition());
final Table main = TableFactory.createSigTable(signatureProfileSettings, MAIN, pdfObject.getStatus(),
requestedSignature);
final IPDFStamper stamper = StamperFactory.createDefaultStamper(pdfObject.getStatus().getSettings());
final IPDFVisualObject visualObject = stamper.createVisualPDFObject(pdfObject, main);
final PositioningInstruction positioningInstruction = Positioning.determineTablePositioning(tablePos,
doc, visualObject, pdfObject.getStatus().getSettings(), signatureProfileSettings);
log.debug("Positioning: {}", positioningInstruction.toString());
if (!isAdobeSigForm) {
if (positioningInstruction.isMakeNewPage()) {
final int last = doc.getNumberOfPages() - 1;
final PDDocumentCatalog root = doc.getDocumentCatalog();
final PDPage lastPage = root.getPages().get(last);
root.getPages().getCOSObject().setNeedToBeUpdated(true);
final PDPage p = new PDPage(lastPage.getMediaBox());
p.setResources(new PDResources());
p.setRotation(lastPage.getRotation());
doc.addPage(p);
}
// handle rotated page
final int targetPageNumber = positioningInstruction.getPage();
log.debug("Target Page: " + targetPageNumber);
final PDPage targetPage = doc.getPages().get(targetPageNumber - 1);
final int rot = targetPage.getRotation();
log.debug("Page rotation: " + rot);
log.debug("resulting Sign rotation: " + positioningInstruction.getRotation());
final SignaturePositionImpl position = new SignaturePositionImpl();
position.setX(positioningInstruction.getX());
position.setY(positioningInstruction.getY());
position.setPage(positioningInstruction.getPage());
position.setHeight(visualObject.getHeight());
position.setWidth(visualObject.getWidth());
requestedSignature.setSignaturePosition(position);
}
properties = new PDFAsVisualSignatureProperties(pdfObject.getStatus().getSettings(), pdfObject,
(PdfBoxVisualObject) visualObject, positioningInstruction, signatureProfileSettings, false);
properties.buildSignature();
if (signatureProfileSettings.isPDFA() || signatureProfileSettings.isPDFA3()) {
final PDDocumentCatalog root = doc.getDocumentCatalog();
InputStream colorProfile = null;
// colorProfile = this.getClass().getResourceAsStream("/icm/sRGB.icm");
colorProfile = this.getClass().getResourceAsStream("/icm/sRGB Color Space Profile.icm");
// Set output intents for PDF/A conformity//
try {
final PDOutputIntent intent = new PDOutputIntent(doc, colorProfile);
intent.setInfo("sRGB IEC61966-2.1");
intent.setOutputCondition("sRGB IEC61966-2.1");
intent.setOutputConditionIdentifier("sRGB IEC61966-2.1");
intent.setRegistryName("http://www.color.org");
final List oi = new ArrayList<>();
oi.add(intent);
root.setOutputIntents(oi);
root.getCOSObject().setNeedToBeUpdated(true);
log.info("added Output Intent");
} catch (final Throwable e) {
e.printStackTrace();
throw new PdfAsException("Failed to add Output Intent", e);
} finally {
IOUtils.closeQuietly(colorProfile);
}
}
options.setPage(positioningInstruction.getPage() - 1);
options.setVisualSignature(properties.getVisibleSignature());
}
doc.addSignature(signature, signer, options);
String sigFieldName = buildNextSignatureFieldName(doc, pdfObject);
// this is not used for Adobe signature fields
if (!isAdobeSigForm) {
PDSignatureField signatureField = null;
final PDAcroForm acroFormm = doc.getDocumentCatalog().getAcroForm();
if (acroFormm != null) {
@SuppressWarnings("unchecked")
final List fields = acroFormm.getFields();
if (fields != null) {
for (final PDField pdField : fields) {
if (pdField != null) {
if (pdField instanceof PDSignatureField) {
final PDSignatureField tmpSigField = (PDSignatureField) pdField;
if (tmpSigField.getSignature() != null
&& tmpSigField.getSignature().getCOSObject() != null) {
if (tmpSigField.getSignature().getCOSObject()
.equals(signature.getCOSObject())) {
signatureField = (PDSignatureField) pdField;
}
}
}
}
}
} else {
log.warn("Failed to name Signature Field! [Cannot find Field list in acroForm!]");
}
if (signatureField != null) {
signatureField.setPartialName(sigFieldName);
}
if (properties != null) {
signatureField.setAlternateFieldName(properties.getAlternativeTableCaption());
} else {
signatureField.setAlternateFieldName(sigFieldName);
}
} else {
log.warn("Failed to name Signature Field! [Cannot find acroForm!]");
}
}
PDSignatureField signatureField = null;
final PDAcroForm acroForm = doc.getDocumentCatalog().getAcroForm();
if (acroForm != null) {
signatureField = (PDSignatureField) acroForm.getField(sigFieldName);
}
injectPdfUaContent(doc, signatureField, sigFieldName, signatureProfileSettings);
performTechnicalSignature(doc, pdfObject, signatureProfileSettings);
log.debug("Signature done!");
} catch (final IOException e) {
log.warn(MessageResolver.resolveMessage("error.pdf.sig.01"), e);
throw new PdfAsException("error.pdf.sig.01", e);
} catch (PDFASError e2) {
log.warn(e2.getInfo());
throw new PdfAsException("error.pdf.sig.01", e2);
} finally {
if (options != null) {
if (options.getVisualSignature() != null) {
try {
options.getVisualSignature().close();
options.close();
} catch (IOException e) {
log.debug("Failed to close VisualSignature!", e);
}
}
}
if (doc != null) {
try {
doc.close();
// SignaturePlaceholderExtractor.getPlaceholders().clear();
} catch (final IOException e) {
log.debug("Failed to close COS Doc!", e);
// Ignore
}
}
}
}
private void performTechnicalSignature(PDDocument doc, PDFBOXObject pdfObject,
SignatureProfileSettings signatureProfileSettings) throws PdfAsException {
try {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
synchronized (doc) {
doc.saveIncremental(bos);
final byte[] outputDocument = bos.toByteArray();
pdfObject.setSignedDocument(outputDocument);
}
/* Check if resulting pdf is PDF-A conform */
if (signatureProfileSettings.isPDFA()) {
runPDFAPreflight(new ByteArrayDataSource(pdfObject.getSignedDocument()));
}
} catch (final IOException e1) {
log.error("Can not save incremental update", e1);
}
System.gc();
}
private void injectPdfUaContent(PDDocument doc, PDSignatureField signatureField, String sigFieldName,
SignatureProfileSettings signatureProfileSettings) throws PdfAsException {
try {
log.info("Adding pdf/ua content .... ");
final PDDocumentCatalog root = doc.getDocumentCatalog();
final PDStructureTreeRoot structureTreeRoot = root.getStructureTreeRoot();
if (structureTreeRoot != null) {
log.info("Tree Root: {}", structureTreeRoot.toString());
final List