001/* Copyright (C) 2014 konik.io
002 *
003 * This file is part of the Konik library.
004 *
005 * The Konik library is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * The Konik library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with the Konik library. If not, see <http://www.gnu.org/licenses/>.
017 */
018package io.konik.carriage.pdfbox;
019
020import static java.util.Collections.singletonMap;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.util.Calendar;
026import java.util.Scanner;
027
028import javax.inject.Named;
029import javax.inject.Singleton;
030import javax.xml.transform.TransformerException;
031
032import org.apache.pdfbox.cos.COSArray;
033import org.apache.pdfbox.cos.COSDictionary;
034import org.apache.pdfbox.pdmodel.PDDocument;
035import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
036import org.apache.pdfbox.pdmodel.PDDocumentInformation;
037import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
038import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
039import org.apache.pdfbox.pdmodel.common.PDMetadata;
040import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
041import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
042import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo;
043import org.apache.xmpbox.XMPMetadata;
044import org.apache.xmpbox.schema.AdobePDFSchema;
045import org.apache.xmpbox.schema.DublinCoreSchema;
046import org.apache.xmpbox.schema.PDFAExtensionSchema;
047import org.apache.xmpbox.schema.PDFAIdentificationSchema;
048import org.apache.xmpbox.schema.XMPBasicSchema;
049import org.apache.xmpbox.schema.XMPSchema;
050import org.apache.xmpbox.type.BadFieldValueException;
051import org.apache.xmpbox.xml.DomXmpParser;
052import org.apache.xmpbox.xml.XmpParsingException;
053import org.apache.xmpbox.xml.XmpSerializer;
054
055import io.konik.carriage.pdfbox.exception.NotPDFAException;
056import io.konik.carriage.pdfbox.xmp.XMPSchemaZugferd1p0;
057import io.konik.carriage.utils.ByteCountingInputStream;
058import io.konik.harness.AppendParameter;
059import io.konik.harness.FileAppender;
060import io.konik.harness.exception.InvoiceAppendError;
061
062/**
063 * ZUGFeRD PDFBox Invoice Appender.
064 */
065@Named
066@Singleton
067public class PDFBoxInvoiceAppender implements FileAppender {
068
069   private static final int PRIORITY = 50;
070   private static final String PRODUCER = "Konik Library with PDFBox-Carriage";
071   private static final String MIME_TYPE = "text/xml";
072   private static final String ZF_FILE_NAME = "ZUGFeRD-invoice.xml";
073   private static final String USER_NAME_KEY = "user.name";
074   private static final String PDF_AUTHOR_KEY = "io.konik.carriage.pdf.author";
075   private final XMPMetadata zfDefaultXmp;
076
077   /**
078    * Instantiates a new PDF box invoice appender.
079    */
080   public PDFBoxInvoiceAppender() {
081      try {
082         InputStream zfExtensionIs = getClass().getResourceAsStream("/zf_extension.pdfbox.xmp");
083         DomXmpParser builder = new DomXmpParser();
084         builder.setStrictParsing(true);
085         zfDefaultXmp = builder.parse(zfExtensionIs);
086         XMPSchema schema = zfDefaultXmp.getSchema(PDFAExtensionSchema.class);
087         schema.addNamespace("http://www.aiim.org/pdfa/ns/schema#", "pdfaSchema");
088         schema.addNamespace("http://www.aiim.org/pdfa/ns/property#", "pdfaProperty");
089      } catch (XmpParsingException e) {
090         throw new InvoiceAppendError("Error initializing PDFBoxInvoiceAppender", e);
091      }
092   }
093
094   @Override
095   public void append(AppendParameter appendParameter) {
096      InputStream inputPdf = appendParameter.inputPdf();
097      PDDocument doc = null;
098      try {
099         doc = PDDocument.load(inputPdf);
100         doc.setAllSecurityToBeRemoved(true);
101         checkisPdfA(doc);
102         convertToPdfA3(doc);
103         setMetadata(doc, appendParameter);
104         attachZugferdFile(doc, appendParameter.attachmentFile());
105         doc.getDocument().setVersion(1.7f);
106         doc.save(appendParameter.resultingPdf());         
107      } catch (Exception e) {
108         throw new InvoiceAppendError("Error appending Invoice the input stream is: " + inputPdf, e);
109      }finally {
110         if (doc != null) try {
111            doc.close();
112         } catch (IOException e) {
113            throw new InvoiceAppendError("Could not close PDF Document", e);
114         }
115      }
116   }
117
118   protected void checkisPdfA(PDDocument doc) {
119      PDMetadata metadata = doc.getDocumentCatalog().getMetadata();
120      if (metadata != null) {
121         try {
122            InputStream inputStream = metadata.createInputStream();
123            Scanner streamScanner = new Scanner(inputStream);
124            String found = streamScanner.findWithinHorizon("http://www.aiim.org/pdfa/ns/id", 0);
125            streamScanner.close();
126            if (found == null) { throw new NotPDFAException(); }
127         } catch (IOException e) {
128            throw new InvoiceAppendError("Could not read PDF Metadata", e);
129         }
130      }
131   }
132
133   protected void convertToPdfA3(PDDocument document) throws Exception {
134
135   }
136
137   private void setMetadata(PDDocument doc, AppendParameter appendParameter) throws IOException, TransformerException,
138         BadFieldValueException {
139      
140      Calendar now = Calendar.getInstance();
141      PDDocumentCatalog catalog = doc.getDocumentCatalog();
142
143      PDMetadata metadata = new PDMetadata(doc);
144      catalog.setMetadata(metadata);
145
146      XMPMetadata xmp = XMPMetadata.createXMPMetadata();
147      PDFAIdentificationSchema pdfaid = new PDFAIdentificationSchema(xmp);
148      pdfaid.setPart(Integer.valueOf(3));
149      pdfaid.setConformance("B");
150      xmp.addSchema(pdfaid);
151
152      DublinCoreSchema dublicCore = new DublinCoreSchema(xmp);
153      xmp.addSchema(dublicCore);
154
155      XMPBasicSchema basicSchema = new XMPBasicSchema(xmp);
156      basicSchema.setCreatorTool(PRODUCER);
157      basicSchema.setCreateDate(now);
158      xmp.addSchema(basicSchema);
159
160      PDDocumentInformation pdi = doc.getDocumentInformation();
161      pdi.setModificationDate(now);
162      pdi.setProducer(PRODUCER);
163      pdi.setAuthor(getAuthor());
164      doc.setDocumentInformation(pdi);
165
166      AdobePDFSchema pdf = new AdobePDFSchema(xmp);
167      pdf.setProducer(PRODUCER);
168      xmp.addSchema(pdf);
169
170      PDMarkInfo markinfo = new PDMarkInfo();
171      markinfo.setMarked(true);
172      doc.getDocumentCatalog().setMarkInfo(markinfo);
173
174      xmp.addSchema(zfDefaultXmp.getPDFExtensionSchema());
175      XMPSchemaZugferd1p0 zf = new XMPSchemaZugferd1p0(xmp);
176      zf.setConformanceLevel(appendParameter.zugferdConformanceLevel());
177      zf.setVersion(appendParameter.zugferdVersion());
178      xmp.addSchema(zf);
179
180      OutputStream outputStreamMeta = metadata.createOutputStream();
181
182      new XmpSerializer().serialize(xmp, outputStreamMeta, true);
183
184      outputStreamMeta.close();
185   }
186
187   private static void attachZugferdFile(PDDocument doc, InputStream zugferdFile) throws IOException {
188      PDEmbeddedFilesNameTreeNode fileNameTreeNode = new PDEmbeddedFilesNameTreeNode();
189
190      PDEmbeddedFile embeddedFile = createEmbeddedFile(doc, zugferdFile);
191      embeddedFile.addCompression();
192      PDComplexFileSpecification fileSpecification = createFileSpecification(embeddedFile);
193
194      COSDictionary dict = fileSpecification.getCOSObject();
195      dict.setName("AFRelationship", "Alternative");
196      dict.setString("UF", ZF_FILE_NAME);
197
198      fileNameTreeNode.setNames(singletonMap(ZF_FILE_NAME, fileSpecification));
199
200      setNamesDictionary(doc, fileNameTreeNode);
201
202      COSArray cosArray = new COSArray();
203      cosArray.add(fileSpecification);
204      doc.getDocumentCatalog().getCOSObject().setItem("AF", cosArray);
205   }
206
207   private static PDComplexFileSpecification createFileSpecification(PDEmbeddedFile embeddedFile) {
208      PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
209      fileSpecification.setFile(ZF_FILE_NAME);
210      fileSpecification.setEmbeddedFile(embeddedFile);
211      fileSpecification.setFileDescription("ZUGFeRD Invoice created with Konik Library");
212      return fileSpecification;
213   }
214
215   private static PDEmbeddedFile createEmbeddedFile(PDDocument doc, InputStream zugferdFile) throws IOException {
216      Calendar now = Calendar.getInstance();
217      ByteCountingInputStream countingIs = new ByteCountingInputStream(zugferdFile);
218      PDEmbeddedFile embeddedFile = new PDEmbeddedFile(doc, countingIs);
219      embeddedFile.addCompression();
220      embeddedFile.setSubtype(MIME_TYPE);
221      embeddedFile.setSize(countingIs.getByteCount());
222      embeddedFile.setCreationDate(now);
223      embeddedFile.setModDate(now);
224      return embeddedFile;
225   }
226
227   private static void setNamesDictionary(PDDocument doc, PDEmbeddedFilesNameTreeNode fileNameTreeNode) {
228      PDDocumentCatalog documentCatalog = doc.getDocumentCatalog();
229      PDDocumentNameDictionary namesDictionary = new PDDocumentNameDictionary(documentCatalog);
230      namesDictionary.setEmbeddedFiles(fileNameTreeNode);
231      documentCatalog.setNames(namesDictionary);
232   }
233
234   private static String getAuthor() {
235      String defaultAuthor = getDefaultAuthor();
236      return Configuration.INSTANCE.getProperty(PDF_AUTHOR_KEY, defaultAuthor);
237
238   }
239
240   private static String getDefaultAuthor() {
241      if (System.getProperty(PDF_AUTHOR_KEY) != null) { return System.getProperty(PDF_AUTHOR_KEY); }
242      return System.getProperty(USER_NAME_KEY);
243   }
244
245   @Override
246   public int getPriority() {
247      return PRIORITY;
248   }
249
250}