From 29ec10fb663523c4a18904c332199ce6e974dd2f Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Wed, 27 Nov 2013 10:05:35 +0100 Subject: Support for QR Placeholders in PAdES --- .../java/at/gv/egiz/pdfas/cli/DeveloperMain.java | 16 +- .../exceptions/PlaceholderExtractionException.java | 11 + pdf-as-lib/bin/placeholder/empty.jpg | Bin 0 -> 631 bytes .../bin/placeholder/pdfbox-reader.properties | 23 ++ pdf-as-lib/build.gradle | 2 + .../at/gv/egiz/pdfas/lib/api/PdfAsFactory.java | 4 +- .../java/at/gv/egiz/pdfas/lib/impl/PdfAsImpl.java | 87 ++++- .../placeholder/SignaturePlaceholderContext.java | 72 +++++ .../impl/placeholder/SignaturePlaceholderData.java | 152 +++++++++ .../placeholder/SignaturePlaceholderExtractor.java | 351 +++++++++++++++++++++ .../pdfas/lib/impl/placeholder/package-info.java | 8 + .../egiz/pdfas/lib/impl/stamping/IPDFStamper.java | 2 +- .../pdfas/lib/impl/status/RequestedSignature.java | 4 + .../egiz/pdfas/lib/impl/verify/IVerifyFilter.java | 2 + .../java/at/gv/egiz/sl/util/BKUSLConnector.java | 2 +- .../egiz/sl/util/ISignatureConnectorSLWrapper.java | 2 +- .../src/main/resources/placeholder/empty.jpg | Bin 0 -> 631 bytes .../resources/placeholder/pdfbox-reader.properties | 23 ++ .../at/gv/egiz/pdfas/sigs/pades/PAdESVerifier.java | 7 +- .../sigs/pkcs7detached/PKCS7DetachedVerifier.java | 6 + .../at/gv/egiz/pdfas/stmp/itext/ITextStamper.java | 14 +- .../com/lowagie/text/pdf/ITextStamperAccess.java | 47 +++ .../java/com/lowagie/text/pdf/package-info.java | 8 + 23 files changed, 818 insertions(+), 25 deletions(-) create mode 100644 pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/exceptions/PlaceholderExtractionException.java create mode 100644 pdf-as-lib/bin/placeholder/empty.jpg create mode 100644 pdf-as-lib/bin/placeholder/pdfbox-reader.properties create mode 100644 pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderContext.java create mode 100644 pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderData.java create mode 100644 pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderExtractor.java create mode 100644 pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/package-info.java create mode 100644 pdf-as-lib/src/main/resources/placeholder/empty.jpg create mode 100644 pdf-as-lib/src/main/resources/placeholder/pdfbox-reader.properties create mode 100644 stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/ITextStamperAccess.java create mode 100644 stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/package-info.java diff --git a/pdf-as-cli/src/main/java/at/gv/egiz/pdfas/cli/DeveloperMain.java b/pdf-as-cli/src/main/java/at/gv/egiz/pdfas/cli/DeveloperMain.java index 4072e496..ed029aac 100644 --- a/pdf-as-cli/src/main/java/at/gv/egiz/pdfas/cli/DeveloperMain.java +++ b/pdf-as-cli/src/main/java/at/gv/egiz/pdfas/cli/DeveloperMain.java @@ -41,16 +41,16 @@ public class DeveloperMain { byte[] data; try { IPlainSigner signer = new PKCS7DetachedSigner(keyStoreFile, keyAlias, keyStorePass, keyPass, keyStoreType); - data = StreamUtils.inputStreamToByteArray(new FileInputStream("/home/afitzek/tmp/pdf-a problem/TestGhostscriptPdfA.pdf")); + data = StreamUtils.inputStreamToByteArray(new FileInputStream("/home/afitzek/qr_2.pdf")); SignParameter parameter = PdfAsFactory.createSignParameter(config, new ByteArrayDataSource(data)); ByteArrayDataSink bads = new ByteArrayDataSink(); parameter.setSignatureProfileId("SIGNATURBLOCK_DE_PDFA"); parameter.setOutput(bads); //parameter.setPlainSigner(new PAdESSigner(new BKUSLConnector(config))); - //parameter.setPlainSigner(signer); - parameter.setPlainSigner(new PAdESSigner(new MOAConnector(config))); - /* - StatusRequest request = pdfas.startSign(parameter); + parameter.setPlainSigner(signer); + //parameter.setPlainSigner(new PAdESSigner(new MOAConnector(config))); + + /*StatusRequest request = pdfas.startSign(parameter); if(request.needCertificate()) { request.setCertificate(signer.getCertificate().getEncoded()); @@ -78,12 +78,12 @@ public class DeveloperMain { } */ pdfas.sign(parameter); - FileOutputStream fos = new FileOutputStream("/home/afitzek/devel/pdfas_neu/simple_out3.pdf"); + FileOutputStream fos = new FileOutputStream("/home/afitzek/qr_2_neu.pdf"); fos.write(bads.getData()); fos.close(); - VerifyParameter verify = new VerifyParameterImpl(config, new ByteArrayDataSource(bads.getData())); - pdfas.verify(verify); + //VerifyParameter verify = new VerifyParameterImpl(config, new ByteArrayDataSource(bads.getData())); + //pdfas.verify(verify); } catch (FileNotFoundException e1) { // TODO Auto-generated catch block diff --git a/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/exceptions/PlaceholderExtractionException.java b/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/exceptions/PlaceholderExtractionException.java new file mode 100644 index 00000000..bef8d245 --- /dev/null +++ b/pdf-as-common/src/main/java/at/gv/egiz/pdfas/common/exceptions/PlaceholderExtractionException.java @@ -0,0 +1,11 @@ +package at.gv.egiz.pdfas.common.exceptions; + +public class PlaceholderExtractionException extends PdfAsException { + public PlaceholderExtractionException(String msgId) { + super(msgId); + } + + public PlaceholderExtractionException(String msgId, Throwable e) { + super(msgId, e); + } +} diff --git a/pdf-as-lib/bin/placeholder/empty.jpg b/pdf-as-lib/bin/placeholder/empty.jpg new file mode 100644 index 00000000..7913177d Binary files /dev/null and b/pdf-as-lib/bin/placeholder/empty.jpg differ diff --git a/pdf-as-lib/bin/placeholder/pdfbox-reader.properties b/pdf-as-lib/bin/placeholder/pdfbox-reader.properties new file mode 100644 index 00000000..a3decc96 --- /dev/null +++ b/pdf-as-lib/bin/placeholder/pdfbox-reader.properties @@ -0,0 +1,23 @@ +BT = org.apache.pdfbox.util.operator.BeginText +cm = org.apache.pdfbox.util.operator.Concatenate +Do = org.apache.pdfbox.util.operator.Invoke +ET = org.apache.pdfbox.util.operator.EndText +gs = org.apache.pdfbox.util.operator.SetGraphicsStateParameters +q = org.apache.pdfbox.util.operator.GSave +Q = org.apache.pdfbox.util.operator.GRestore +T* = org.apache.pdfbox.util.operator.NextLine +Tc = org.apache.pdfbox.util.operator.SetCharSpacing +Td = org.apache.pdfbox.util.operator.MoveText +TD = org.apache.pdfbox.util.operator.MoveTextSetLeading +Tf = org.apache.pdfbox.util.operator.SetTextFont +Tj = org.apache.pdfbox.util.operator.ShowText +TJ = org.apache.pdfbox.util.operator.ShowTextGlyph +TL = org.apache.pdfbox.util.operator.SetTextLeading +Tm = org.apache.pdfbox.util.operator.SetMatrix +Tr = org.apache.pdfbox.util.operator.SetTextRenderingMode +Ts = org.apache.pdfbox.util.operator.SetTextRise +Tw = org.apache.pdfbox.util.operator.SetWordSpacing +Tz = org.apache.pdfbox.util.operator.SetHorizontalTextScaling +w = org.apache.pdfbox.util.operator.SetLineWidth +\' = org.apache.pdfbox.util.operator.MoveAndShow +\" = org.apache.pdfbox.util.operator.SetMoveAndShow \ No newline at end of file diff --git a/pdf-as-lib/build.gradle b/pdf-as-lib/build.gradle index 68d65d99..683df99c 100644 --- a/pdf-as-lib/build.gradle +++ b/pdf-as-lib/build.gradle @@ -67,6 +67,8 @@ dependencies { compile group: 'eu.europa.ec.joinup.egovlabs.pdf-as.iaik', name: 'iaik_ecc_eval_signed', version: '2.19' compile group: 'log4j', name: 'log4j', version: '1.2.17' compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.5' + compile group: 'com.google.zxing', name: 'core', version: '2.2' + compile group: 'com.google.zxing', name: 'javase', version: '2.2' //compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.5' testCompile group: 'junit', name: 'junit', version: '4.+' /*generateJavaFromWsdlDeps('org.apache.axis:axis:1.4') diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/api/PdfAsFactory.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/api/PdfAsFactory.java index 10391ecc..e26e3fdb 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/api/PdfAsFactory.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/api/PdfAsFactory.java @@ -14,8 +14,8 @@ import at.gv.egiz.pdfas.lib.impl.VerifyParameterImpl; public class PdfAsFactory { static { - //PropertyConfigurator.configure(ClassLoader.getSystemResourceAsStream("resources/log4j.properties")); - BasicConfigurator.configure(); + PropertyConfigurator.configure(ClassLoader.getSystemResourceAsStream("resources/log4j.properties")); + //BasicConfigurator.configure(); } public static PdfAs createPdfAs(File configuration) { diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/PdfAsImpl.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/PdfAsImpl.java index 4a8e41c3..2f2d47c8 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/PdfAsImpl.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/PdfAsImpl.java @@ -3,6 +3,7 @@ package at.gv.egiz.pdfas.lib.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -35,6 +36,8 @@ import at.gv.egiz.pdfas.lib.api.verify.VerifyResult; import at.gv.egiz.pdfas.lib.impl.configuration.ConfigurationImpl; import at.gv.egiz.pdfas.lib.impl.configuration.PlaceholderConfiguration; import at.gv.egiz.pdfas.lib.impl.configuration.SignatureProfileConfiguration; +import at.gv.egiz.pdfas.lib.impl.placeholder.SignaturePlaceholderData; +import at.gv.egiz.pdfas.lib.impl.placeholder.SignaturePlaceholderExtractor; import at.gv.egiz.pdfas.lib.impl.positioning.Positioning; import at.gv.egiz.pdfas.lib.impl.signing.IPdfSigner; import at.gv.egiz.pdfas.lib.impl.signing.PdfSignerFactory; @@ -105,13 +108,12 @@ public class PdfAsImpl implements PdfAs, IConfigurationConstants { status.getPdfObject().setOriginalDocument( parameter.getDataSource().getByteData()); - // Placeholder search? - if (placeholderConfiguration.isGlobalPlaceholderEnabled()) { - // TODO: Do placeholder search - } - this.stampPdf(status); + FileOutputStream fos = new FileOutputStream("/home/afitzek/qr_2_stamped.pdf"); + fos.write(status.getPdfObject().getStampedDocument()); + fos.close(); + /* * if (requestedSignature.isVisual()) { * logger.info("Creating visual siganture block"); // @@ -259,7 +261,7 @@ public class PdfAsImpl implements PdfAs, IConfigurationConstants { if (verifyFilter != null) { List results = verifyFilter.verify( contentData.toByteArray(), content.getBytes()); - if(results != null && !results.isEmpty()) { + if (results != null && !results.isEmpty()) { result.addAll(results); } } @@ -421,6 +423,72 @@ public class PdfAsImpl implements PdfAs, IConfigurationConstants { } } + private boolean checkPlaceholderSignature(OperationStatus status) + throws PdfAsException, IOException { + if (status.getPlaceholderConfiguration().isGlobalPlaceholderEnabled()) { + SignaturePlaceholderData signaturePlaceholderData = SignaturePlaceholderExtractor + .extract(new ByteArrayInputStream(status.getPdfObject() + .getOriginalDocument()), null, 1); + + if (signaturePlaceholderData != null) { + RequestedSignature requestedSignature = status + .getRequestedSignature(); + + if (signaturePlaceholderData.getProfile() != null) { + requestedSignature + .setSignatureProfileID(signaturePlaceholderData + .getProfile()); + } + + String signatureProfileID = requestedSignature + .getSignatureProfileID(); + + TablePos tablePos = signaturePlaceholderData.getTablePos(); + + SignatureProfileSettings signatureProfileSettings = TableFactory + .createProfile(signatureProfileID, settings); + + Table main = TableFactory.createSigTable( + signatureProfileSettings, MAIN, settings, + requestedSignature); + + IPDFStamper stamper = StamperFactory + .createDefaultStamper(settings); + IPDFVisualObject visualObject = stamper.createVisualPDFObject( + status.getPdfObject(), main); + + PDDocument originalDocument = PDDocument + .load(new ByteArrayInputStream(status.getPdfObject() + .getOriginalDocument())); + + PositioningInstruction positioningInstruction = Positioning + .determineTablePositioning(tablePos, "", originalDocument, + visualObject); + + // ================================================================ + // StampingStage (visual) -> stamp logical signature block to + // location (itext) + + byte[] incrementalUpdate = stamper.writeVisualObject(visualObject, + positioningInstruction, status.getPdfObject() + .getOriginalDocument(), signaturePlaceholderData.getPlaceholderName()); + + 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); + + status.getPdfObject().setStampedDocument(incrementalUpdate); + return true; + } + } + return false; + } + private void stampPdf(OperationStatus status) throws PdfAsException, IOException { @@ -429,6 +497,11 @@ public class PdfAsImpl implements PdfAs, IConfigurationConstants { SignatureProfileConfiguration signatureProfileConfiguration = status .getSignatureProfileConfiguration(signatureProfileID); + if (checkPlaceholderSignature(status)) { + logger.info("Placeholder found for Signature"); + return; + } + if (requestedSignature.isVisual()) { logger.info("Creating visual siganture block"); // ================================================================ @@ -477,7 +550,7 @@ public class PdfAsImpl implements PdfAs, IConfigurationConstants { byte[] incrementalUpdate = stamper.writeVisualObject(visualObject, positioningInstruction, status.getPdfObject() - .getOriginalDocument()); + .getOriginalDocument(), null); SignaturePositionImpl position = new SignaturePositionImpl(); position.setX(positioningInstruction.getX()); diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderContext.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderContext.java new file mode 100644 index 00000000..28a34f6a --- /dev/null +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderContext.java @@ -0,0 +1,72 @@ +/** + * Copyright 2006 by Know-Center, 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.placeholder; + +/** + * Store and retrieve {@link SignaturePlaceholderData} in/from a thread local context. + * + * @author exthex + * + */ +public class SignaturePlaceholderContext { + + private ThreadLocal sigHolder = new ThreadLocal(); + + private static SignaturePlaceholderContext instance = new SignaturePlaceholderContext(); + + /** + * Constructor. Private because this is a singleton. + */ + private SignaturePlaceholderContext() { + + } + + /** + * Get the {@link SignaturePlaceholderData} which is currently bound to this thread. + * Might be null. + * + * @return + */ + public static SignaturePlaceholderData getSignaturePlaceholderData(){ + return (SignaturePlaceholderData)instance.sigHolder.get(); + } + + /** + * + * @return true if there is currently a {@link SignaturePlaceholderData} bound to this thread, false otherwise. + */ + public static boolean isSignaturePlaceholderDataSet() { + return instance.sigHolder.get() != null; + } + + /** + * Bind a {@link SignaturePlaceholderData} to this thread. + * If the given data is null, the context will be cleared. + * + * @param data if null, clears the ThreadLocal, else binds the data to the current thread. + */ + public static void setSignaturePlaceholderData(SignaturePlaceholderData data) { + instance.sigHolder.set(data); + } +} diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderData.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderData.java new file mode 100644 index 00000000..d068104a --- /dev/null +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderData.java @@ -0,0 +1,152 @@ +/** + * Copyright 2006 by Know-Center, 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.placeholder; + +import at.knowcenter.wag.egov.egiz.pdf.TablePos; + +/** + * This class represents all the data which can be extracted from a placeholder image. + * + * @author exthex + * + */ +public class SignaturePlaceholderData { + + public static final String ID_KEY = "id"; + + public static final String PROFILE_KEY = "profile"; + + public static final String TYPE_KEY = "type"; + + public static final String SIG_KEY_KEY = "key"; + + private String profile; + + private String type; + + private String key; + + private String id; + + private TablePos tablePos; + + private String placeholderName; + + /** + * + * @param profile + * @param type + * @param sigKey + * @param id + */ + public SignaturePlaceholderData(String profile, String type, String sigKey, String id) { + this.profile = profile; + this.type = type; + this.key = sigKey; + this.id = id; + } + + /** + * Get the table position for the signature block.
+ * The table position is created from the page number, the upper left corner and the width of the placeholder image. + * + * @return + */ + public TablePos getTablePos() { + return tablePos; + } + + void setTablePos(TablePos tablePos) { + this.tablePos = tablePos; + } + + /** + * The profile name. Might be null if not included in the qr-code. + * + * @return + */ + public String getProfile() { + return profile; + } + + void setProfile(String profile) { + this.profile = profile; + } + + /** + * The signature type: "textual" or "binary". Might be null if not included in the qr-code. + * @return + */ + public String getType() { + return type; + } + + void setType(String type) { + this.type = type; + } + + /** + * The key identifier for MOA signature. Might be null if not included in the qr-code. + * + * @return + */ + public String getKey() { + return key; + } + + void setKey(String key) { + this.key = key; + } + + public String toString() { + return getClass().toString() + ": profile=" + profile + "; type=" + type + "; sigKey=" + key + "; table pos=" + tablePos; + } + + void setPlaceholderName(String name) { + this.placeholderName = name; + } + + /** + * The name of the placeholder image. + * + * @return + */ + public String getPlaceholderName() { + return placeholderName; + } + + /** + * The id associated with this placeholder. + * + * @return + */ + public String getId() { + return id; + } + + void setId(String id) { + this.id = id; + } + +} diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderExtractor.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderExtractor.java new file mode 100644 index 00000000..eabc27e2 --- /dev/null +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/SignaturePlaceholderExtractor.java @@ -0,0 +1,351 @@ +/** + * Copyright 2006 by Know-Center, 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.placeholder; + +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.exceptions.WrappedIOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectImage; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.util.PDFOperator; +import org.apache.pdfbox.util.PDFStreamEngine; +import org.apache.pdfbox.util.ResourceLoader; + +import at.gv.egiz.pdfas.common.exceptions.PDFIOException; +import at.gv.egiz.pdfas.common.exceptions.PdfAsException; +import at.gv.egiz.pdfas.common.exceptions.PlaceholderExtractionException; +import at.knowcenter.wag.egov.egiz.pdf.TablePos; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.NotFoundException; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; + +////// + + + +/** + * Extract all relevant information from a placeholder image. + * + * @author exthex + * + */ +public class SignaturePlaceholderExtractor extends PDFStreamEngine { + /** + * The log. + */ + private static Log log = LogFactory.getLog(SignaturePlaceholderExtractor.class); + + public static final String QR_PLACEHOLDER_IDENTIFIER = "PDF-AS-POS"; + public static final int PLACEHOLDER_MATCH_MODE_STRICT = 0; + public static final int PLACEHOLDER_MATCH_MODE_MODERATE = 1; + public static final int PLACEHOLDER_MATCH_MODE_LENIENT = 2; + + private List placeholders = new Vector(); + private int currentPage = 0; + + private SignaturePlaceholderExtractor(String placeholderId, int placeholderMatchMode) throws IOException { + super(ResourceLoader.loadProperties("placeholder/pdfbox-reader.properties", + true)); + } + + /** + * Search the document for placeholder images and possibly included + * additional info.
+ * Searches only for the first placeholder page after page from top. + * + * @param inputStream + * @return all available info from the first found placeholder. + * @throws PDFDocumentException if the document could not be read. + * @throws PlaceholderExtractionException if STRICT matching mode was requested and no suitable placeholder could be found. + */ + public static SignaturePlaceholderData extract(InputStream inputStream, String placeholderId, int matchMode) + throws PdfAsException { + SignaturePlaceholderContext.setSignaturePlaceholderData(null); + PDDocument doc = null; + try + { + try { + doc = PDDocument.load(inputStream); + } catch (IOException e) { + throw new PDFIOException("failed to read document", e); + } + SignaturePlaceholderExtractor extractor; + try + { + extractor = new SignaturePlaceholderExtractor(placeholderId, matchMode); + } catch (IOException e2) { + throw new PDFIOException("failed to read document", e2); + } + List pages = doc.getDocumentCatalog().getAllPages(); + Iterator iter = pages.iterator(); + int pageNr = 0; + while (iter.hasNext()) { + pageNr++; + PDPage page = (PDPage) iter.next(); + try { + extractor.setCurrentPage(pageNr); + extractor.processStream( page, page.findResources(), page.getContents().getStream() ); + SignaturePlaceholderData ret = matchPlaceholderPage(extractor.placeholders, placeholderId, matchMode); + if (ret != null){ + SignaturePlaceholderContext.setSignaturePlaceholderData(ret); + return ret; + } + } catch (IOException e1) { + throw new PDFIOException("failed to read document", e1); + } + + } + if (extractor.placeholders.size() > 0){ + SignaturePlaceholderData ret = matchPlaceholderDocument(extractor.placeholders, placeholderId, matchMode); + SignaturePlaceholderContext.setSignaturePlaceholderData(ret); + return ret; + } + // no placeholders found, apply strict mode if set + if (matchMode == PLACEHOLDER_MATCH_MODE_STRICT) { + throw new PlaceholderExtractionException("no suitable placeholder found and STRICT matching mode requested."); + } + + return null; + } finally { + if (doc != null) + try { + doc.close(); + } catch (IOException e) { + log.debug("Could not close document.", e); + } + } + + } + + private static SignaturePlaceholderData matchPlaceholderDocument( + List placeholders, String placeholderId, int matchMode) throws PlaceholderExtractionException { + + if (matchMode == PLACEHOLDER_MATCH_MODE_STRICT) + throw new PlaceholderExtractionException("no suitable placeholder found and STRICT matching mode requested."); + + if (placeholders.size() == 0) + return null; + + for (int i = 0; i < placeholders.size(); i++) + { + SignaturePlaceholderData spd = (SignaturePlaceholderData)placeholders.get(i); + if (spd.getId() == null) + return spd; + } + + if (matchMode == PLACEHOLDER_MATCH_MODE_LENIENT) + return (SignaturePlaceholderData)placeholders.get(0); + + return null; + } + + private static SignaturePlaceholderData matchPlaceholderPage(List placeholders, + String placeholderId, int matchMode) { + if (placeholders.size() == 0) + return null; + for (int i = 0; i < placeholders.size(); i++) + { + SignaturePlaceholderData data = (SignaturePlaceholderData)placeholders.get(i); + if (placeholderId != null && placeholderId.equals(data.getId())) + return data; + if (placeholderId == null && data.getId() == null) + return data; + } + return null; + } + + private void setCurrentPage(int pageNr) { + this.currentPage = pageNr; + } + + protected void processOperator( PDFOperator operator, List arguments ) throws IOException + { + String operation = operator.getOperation(); + if( operation.equals( "Do" ) ) + { + COSName objectName = (COSName)arguments.get( 0 ); + Map xobjects = getResources().getXObjects(); + PDXObject xobject = (PDXObject)xobjects.get( objectName.getName() ); + if( xobject instanceof PDXObjectImage ) + { + try + { + PDXObjectImage image = (PDXObjectImage)xobject; + SignaturePlaceholderData data = checkImage(image); + if (data != null) + { + PDPage page = getCurrentPage(); + Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); + double rotationInRadians = (page.findRotation() * Math.PI)/180; + + AffineTransform rotation = new AffineTransform(); + rotation.setToRotation( rotationInRadians ); + AffineTransform rotationInverse = rotation.createInverse(); + Matrix rotationInverseMatrix = new Matrix(); + rotationInverseMatrix.setFromAffineTransform( rotationInverse ); + Matrix rotationMatrix = new Matrix(); + rotationMatrix.setFromAffineTransform( rotation ); + + Matrix unrotatedCTM = ctm.multiply( rotationInverseMatrix ); + + float x = unrotatedCTM.getXPosition(); + float y = unrotatedCTM.getYPosition() + unrotatedCTM.getYScale(); + float w = unrotatedCTM.getXScale(); + + String posString = "p:" + currentPage + ";x:" + x + ";y:" + y + ";w:" + w; + try + { + data.setTablePos(new TablePos(posString)); + data.setPlaceholderName(objectName.getName()); + placeholders.add(data); + } catch (PdfAsException e) { + throw new WrappedIOException(e); + } + } + } + catch( NoninvertibleTransformException e ) + { + throw new WrappedIOException( e ); + } + } + } + else + { + super.processOperator( operator, arguments ); + } + } + + /** + * Checks an image if it is a placeholder for a signature. + * + * @param image + * @return + * @throws IOException + */ + private SignaturePlaceholderData checkImage(PDXObjectImage image) throws IOException { + BufferedImage bimg = image.getRGBImage(); + if (bimg == null) { + String type = image.getSuffix(); + if (type != null) { + type = type.toUpperCase() + " images"; + } else { + type = "Image type"; + } + log.info("Unable to extract image for QRCode analysis. " + type + " not supported. Add additional JAI Image filters to your classpath. Refer to https://jai.dev.java.net. Skipping image."); + return null; + } + if(bimg.getHeight() < 10 || bimg.getWidth() < 10) { + log.debug("Image too small for QRCode. Skipping image."); + return null; + } + + LuminanceSource source = new BufferedImageLuminanceSource(bimg); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + Result result; + long before = System.currentTimeMillis(); + try { + Hashtable hints = new Hashtable(); + Vector formats = new Vector(); + formats.add(BarcodeFormat.QR_CODE); + hints.put(DecodeHintType.POSSIBLE_FORMATS, formats); + result = new MultiFormatReader().decode(bitmap, hints); + + String text = result.getText(); + String profile = null; + String type = null; + String sigKey = null; + String id = null; + if (text != null) { + if (text.startsWith(QR_PLACEHOLDER_IDENTIFIER)) { + String[] data = text.split(";"); + if (data.length > 1) { + for (int i = 1; i < data.length; i++) { + String kvPair = data[i]; + String[] kv = kvPair.split("="); + if (kv.length != 2) { + log.debug("Invalid parameter in placeholder data: " + kvPair); + } else { + if (kv[0].equalsIgnoreCase(SignaturePlaceholderData.ID_KEY)) { + id = kv[1]; + } else if (kv[0].equalsIgnoreCase(SignaturePlaceholderData.PROFILE_KEY)) { + profile = kv[1]; + } else if (kv[0] + .equalsIgnoreCase(SignaturePlaceholderData.SIG_KEY_KEY)) { + sigKey = kv[1]; + } else if (kv[0] + .equalsIgnoreCase(SignaturePlaceholderData.TYPE_KEY)) { + type = kv[1]; + } + } + } + } + return new SignaturePlaceholderData(profile, type, sigKey, id); + } else { + log.warn("QR-Code found but does not start with \"" + QR_PLACEHOLDER_IDENTIFIER + "\". Ignoring QR placeholder."); + } + } + } catch (ReaderException re) { + if (log.isDebugEnabled()) { + log.debug("Could not decode - not a placeholder. needed: " + + (System.currentTimeMillis() - before)); + } + if (!(re instanceof NotFoundException)){ + if (log.isInfoEnabled()) { + log.info("Failed to decode image", re); + } + } + } catch(ArrayIndexOutOfBoundsException e){ + if (log.isInfoEnabled()) { + log.info("Failed to decode image. Probably a zxing bug", e); + } + } + return null; + } + +} + + diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/package-info.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/package-info.java new file mode 100644 index 00000000..815565da --- /dev/null +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/placeholder/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author afitzek + * + */ +package at.gv.egiz.pdfas.lib.impl.placeholder; \ No newline at end of file diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/stamping/IPDFStamper.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/stamping/IPDFStamper.java index fce18ce7..5c84acfe 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/stamping/IPDFStamper.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/stamping/IPDFStamper.java @@ -9,7 +9,7 @@ import at.knowcenter.wag.egov.egiz.table.Table; public interface IPDFStamper { public IPDFVisualObject createVisualPDFObject(PDFObject pdf, Table table); public byte[] writeVisualObject(IPDFVisualObject visualObject, PositioningInstruction positioningInstruction, - byte[] pdfData) throws PdfAsException; + byte[] pdfData, String placeholderName) throws PdfAsException; public void setSettings(ISettings settings); } diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/status/RequestedSignature.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/status/RequestedSignature.java index 8bf56259..8ae8e377 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/status/RequestedSignature.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/status/RequestedSignature.java @@ -50,6 +50,10 @@ public class RequestedSignature { return this.signatureProfile; } + public void setSignatureProfileID(String signatureProfile) { + this.signatureProfile = signatureProfile; + } + public X509Certificate getCertificate() { return this.certificate; } diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/IVerifyFilter.java b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/IVerifyFilter.java index 7aca582b..53c2e342 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/IVerifyFilter.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/pdfas/lib/impl/verify/IVerifyFilter.java @@ -3,9 +3,11 @@ package at.gv.egiz.pdfas.lib.impl.verify; import java.util.List; import at.gv.egiz.pdfas.common.exceptions.PdfAsException; +import at.gv.egiz.pdfas.lib.api.Configuration; import at.gv.egiz.pdfas.lib.api.verify.VerifyResult; public interface IVerifyFilter { + public void setConfiguration(Configuration config); public List verify(byte[] contentData, byte[] signatureContent) throws PdfAsException; public List getFiters(); } diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/BKUSLConnector.java b/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/BKUSLConnector.java index 3381dca5..deecae21 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/BKUSLConnector.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/BKUSLConnector.java @@ -130,7 +130,7 @@ public class BKUSLConnector extends BaseSLConnector { try { slRequest = SLMarschaller.marshalToString(of .createCreateCMSSignatureRequest(request)); - logger.trace(slRequest); + logger.debug(slRequest); String slResponse = performHttpRequestToBKU(slRequest); diff --git a/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/ISignatureConnectorSLWrapper.java b/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/ISignatureConnectorSLWrapper.java index 491c465a..8a7950a4 100644 --- a/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/ISignatureConnectorSLWrapper.java +++ b/pdf-as-lib/src/main/java/at/gv/egiz/sl/util/ISignatureConnectorSLWrapper.java @@ -47,7 +47,7 @@ public class ISignatureConnectorSLWrapper implements ISignatureConnector { } public byte[] sign(byte[] input, int[] byteRange) throws PdfAsException { - CreateCMSSignatureRequestType request = connector.createCMSRequest(input, byteRange); + CreateCMSSignatureRequestType request = connector.createCMSRequest(input, byteRange); CreateCMSSignatureResponseType response = connector.sendCMSRequest(request); return response.getCMSSignature(); diff --git a/pdf-as-lib/src/main/resources/placeholder/empty.jpg b/pdf-as-lib/src/main/resources/placeholder/empty.jpg new file mode 100644 index 00000000..7913177d Binary files /dev/null and b/pdf-as-lib/src/main/resources/placeholder/empty.jpg differ diff --git a/pdf-as-lib/src/main/resources/placeholder/pdfbox-reader.properties b/pdf-as-lib/src/main/resources/placeholder/pdfbox-reader.properties new file mode 100644 index 00000000..a3decc96 --- /dev/null +++ b/pdf-as-lib/src/main/resources/placeholder/pdfbox-reader.properties @@ -0,0 +1,23 @@ +BT = org.apache.pdfbox.util.operator.BeginText +cm = org.apache.pdfbox.util.operator.Concatenate +Do = org.apache.pdfbox.util.operator.Invoke +ET = org.apache.pdfbox.util.operator.EndText +gs = org.apache.pdfbox.util.operator.SetGraphicsStateParameters +q = org.apache.pdfbox.util.operator.GSave +Q = org.apache.pdfbox.util.operator.GRestore +T* = org.apache.pdfbox.util.operator.NextLine +Tc = org.apache.pdfbox.util.operator.SetCharSpacing +Td = org.apache.pdfbox.util.operator.MoveText +TD = org.apache.pdfbox.util.operator.MoveTextSetLeading +Tf = org.apache.pdfbox.util.operator.SetTextFont +Tj = org.apache.pdfbox.util.operator.ShowText +TJ = org.apache.pdfbox.util.operator.ShowTextGlyph +TL = org.apache.pdfbox.util.operator.SetTextLeading +Tm = org.apache.pdfbox.util.operator.SetMatrix +Tr = org.apache.pdfbox.util.operator.SetTextRenderingMode +Ts = org.apache.pdfbox.util.operator.SetTextRise +Tw = org.apache.pdfbox.util.operator.SetWordSpacing +Tz = org.apache.pdfbox.util.operator.SetHorizontalTextScaling +w = org.apache.pdfbox.util.operator.SetLineWidth +\' = org.apache.pdfbox.util.operator.MoveAndShow +\" = org.apache.pdfbox.util.operator.SetMoveAndShow \ No newline at end of file diff --git a/signature-standards/sigs-pades/src/main/java/at/gv/egiz/pdfas/sigs/pades/PAdESVerifier.java b/signature-standards/sigs-pades/src/main/java/at/gv/egiz/pdfas/sigs/pades/PAdESVerifier.java index 71b24213..6af44b59 100644 --- a/signature-standards/sigs-pades/src/main/java/at/gv/egiz/pdfas/sigs/pades/PAdESVerifier.java +++ b/signature-standards/sigs-pades/src/main/java/at/gv/egiz/pdfas/sigs/pades/PAdESVerifier.java @@ -43,8 +43,6 @@ public class PAdESVerifier implements IVerifyFilter { public PAdESVerifier(Configuration config) { IAIK.getInstance(); ECCProvider.addAsProvider(); - this.moaEndpoint = config.getValue(MOA_VERIFY_URL); - this.moaTrustProfile = config.getValue(MOA_VERIFY_TRUSTPROFILE); } @SuppressWarnings("rawtypes") @@ -146,4 +144,9 @@ public class PAdESVerifier implements IVerifyFilter { return result; } + public void setConfiguration(Configuration config) { + this.moaEndpoint = config.getValue(MOA_VERIFY_URL); + this.moaTrustProfile = config.getValue(MOA_VERIFY_TRUSTPROFILE); + } + } diff --git a/signature-standards/sigs-pcks7detached/src/main/java/at/gv/egiz/pdfas/sigs/pkcs7detached/PKCS7DetachedVerifier.java b/signature-standards/sigs-pcks7detached/src/main/java/at/gv/egiz/pdfas/sigs/pkcs7detached/PKCS7DetachedVerifier.java index 7807850b..7e974f3f 100644 --- a/signature-standards/sigs-pcks7detached/src/main/java/at/gv/egiz/pdfas/sigs/pkcs7detached/PKCS7DetachedVerifier.java +++ b/signature-standards/sigs-pcks7detached/src/main/java/at/gv/egiz/pdfas/sigs/pkcs7detached/PKCS7DetachedVerifier.java @@ -14,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import at.gv.egiz.pdfas.common.exceptions.PdfAsException; +import at.gv.egiz.pdfas.lib.api.Configuration; import at.gv.egiz.pdfas.lib.api.verify.VerifyResult; import at.gv.egiz.pdfas.lib.impl.verify.FilterEntry; import at.gv.egiz.pdfas.lib.impl.verify.IVerifyFilter; @@ -71,4 +72,9 @@ public class PKCS7DetachedVerifier implements IVerifyFilter { return result; } + public void setConfiguration(Configuration config) { + // TODO Auto-generated method stub + + } + } diff --git a/stamper/stmp-itext/src/main/java/at/gv/egiz/pdfas/stmp/itext/ITextStamper.java b/stamper/stmp-itext/src/main/java/at/gv/egiz/pdfas/stmp/itext/ITextStamper.java index ff68dcd8..b0801a9c 100644 --- a/stamper/stmp-itext/src/main/java/at/gv/egiz/pdfas/stmp/itext/ITextStamper.java +++ b/stamper/stmp-itext/src/main/java/at/gv/egiz/pdfas/stmp/itext/ITextStamper.java @@ -2,6 +2,7 @@ package at.gv.egiz.pdfas.stmp.itext; import at.gv.egiz.pdfas.common.exceptions.PdfAsException; import at.gv.egiz.pdfas.common.settings.ISettings; +import at.gv.egiz.pdfas.lib.impl.placeholder.SignaturePlaceholderData; 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.status.PDFObject; @@ -444,7 +445,7 @@ public class ITextStamper implements IPDFStamper { } public byte[] writeVisualObject(IPDFVisualObject visualObject, PositioningInstruction positioningInstruction, - byte[] pdfData) throws PdfAsException { + byte[] pdfData, String placeholderName) throws PdfAsException { try { ITextVisualObject object = null; @@ -479,14 +480,21 @@ public class ITextStamper implements IPDFStamper { throw new PdfAsException("The provided page (=" + positioningInstruction.getPage() + ") is out of range."); } - + + if(placeholderName != null) { + ITextStamperAccess.replacePlaceholder(stamper, targetPage, placeholderName); + } + PdfContentByte content = stamper.getOverContent(targetPage); PdfPTable table = object.getTable(); + logger.info("Visual Object: " + visualObject.getWidth() + " x " + visualObject.getHeight()); + //PdfTemplate table_template = content.createTemplate(visualObject.getWidth(), visualObject.getHeight()); + table.writeSelectedRows(0, -1, positioningInstruction.getX(), positioningInstruction.getY(), content); - + stamper.close(); baos.close(); diff --git a/stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/ITextStamperAccess.java b/stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/ITextStamperAccess.java new file mode 100644 index 00000000..65b4ba77 --- /dev/null +++ b/stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/ITextStamperAccess.java @@ -0,0 +1,47 @@ +package com.lowagie.text.pdf; + +import java.io.IOException; +import java.net.MalformedURLException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import at.gv.egiz.pdfas.common.exceptions.PdfAsException; +import at.gv.egiz.pdfas.stmp.itext.ITextStamper; + +import com.lowagie.text.BadElementException; +import com.lowagie.text.Image; + +public class ITextStamperAccess { + + private static final Logger logger = LoggerFactory.getLogger(ITextStamperAccess.class); + + public static void replacePlaceholder(PdfStamper stamper, int pageNr, String placeholderName) + throws BadElementException, MalformedURLException, IOException, + BadPdfFormatException, PdfAsException { + Image img = Image.getInstance(ITextStamper.class.getResource("/placeholder/empty.jpg")); + PdfImage pImg = new PdfImage(img, "Imwurscht", null); + PdfStamperImp stamperImp = (PdfStamperImp)stamper.getWriter(); + PdfIndirectObject ind = stamperImp.addToBody(pImg); + + PdfDictionary resources = stamper.getReader().getPageN(pageNr).getAsDict(PdfName.RESOURCES); + if (ind != null && resources != null) + { + PdfDictionary xobjDict = resources.getAsDict(PdfName.XOBJECT); + if (xobjDict != null) + { + xobjDict.put(new PdfName(placeholderName), ind.getIndirectReference()); + stamperImp.markUsed(resources); + } + else + { + throw new PdfAsException("failed to write PDF", new NullPointerException("Image dictionary not found in document structure!")); + } + } + else + { + throw new PdfAsException("failed to write PDF", new NullPointerException("Resource dictionary not found in document structure!")); + } + } + +} diff --git a/stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/package-info.java b/stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/package-info.java new file mode 100644 index 00000000..131f9340 --- /dev/null +++ b/stamper/stmp-itext/src/main/java/com/lowagie/text/pdf/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author afitzek + * + */ +package com.lowagie.text.pdf; \ No newline at end of file -- cgit v1.2.3