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;
021import io.konik.carriage.pdfbox.xmp.XMPSchemaZugferd1p0;
022import io.konik.carriage.utils.ByteCountingInputStream;
023import io.konik.harness.AppendParameter;
024import io.konik.harness.FileAppender;
025import io.konik.harness.exception.InvoiceAppendError;
026
027import java.io.IOException;
028import java.io.InputStream;
029import java.util.Calendar;
030
031import javax.inject.Named;
032import javax.inject.Singleton;
033import javax.xml.transform.TransformerException;
034
035import org.apache.pdfbox.cos.COSArray;
036import org.apache.pdfbox.cos.COSDictionary;
037import org.apache.pdfbox.pdmodel.PDDocument;
038import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
039import org.apache.pdfbox.pdmodel.PDDocumentInformation;
040import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
041import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
042import org.apache.pdfbox.pdmodel.common.PDMetadata;
043import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
044import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
045import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo;
046import org.apache.xmpbox.XMPMetadata;
047import org.apache.xmpbox.schema.AdobePDFSchema;
048import org.apache.xmpbox.schema.DublinCoreSchema;
049import org.apache.xmpbox.schema.PDFAExtensionSchema;
050import org.apache.xmpbox.schema.PDFAIdentificationSchema;
051import org.apache.xmpbox.schema.XMPBasicSchema;
052import org.apache.xmpbox.schema.XMPSchema;
053import org.apache.xmpbox.type.BadFieldValueException;
054import org.apache.xmpbox.xml.DomXmpParser;
055import org.apache.xmpbox.xml.XmpParsingException;
056import org.apache.xmpbox.xml.XmpSerializationException;
057import org.apache.xmpbox.xml.XmpSerializer;
058
059/**
060 * ZUGFeRD PDFBox Invoice Appender.
061 */
062@Named
063@Singleton
064public class PDFBoxInvoiceAppender implements FileAppender {
065
066   private static final String PRODUCER = "Konik PDFBox-Carriage";
067   private static final String MIME_TYPE = "text/xml";
068   private static final String ZF_FILE_NAME = "ZUGFeRD-invoice.xml";
069   private final XMPMetadata zfDefaultXmp;
070
071   /**
072    * Instantiates a new PDF box invoice appender.
073    */
074   public PDFBoxInvoiceAppender() {
075      try {
076         InputStream zfExtensionIs = getClass().getResourceAsStream("/zf_extension.pdfbox.xmp");
077         DomXmpParser builder = new DomXmpParser();
078         builder.setStrictParsing(true);
079         zfDefaultXmp = builder.parse(zfExtensionIs);
080         XMPSchema schema = zfDefaultXmp.getSchema(PDFAExtensionSchema.class);
081         schema.addNamespace("http://www.aiim.org/pdfa/ns/schema#", "pdfaSchema");
082         schema.addNamespace("http://www.aiim.org/pdfa/ns/property#", "pdfaProperty");
083      } catch (XmpParsingException e) {
084         throw new InvoiceAppendError("Error initializing PDFBoxInvoiceAppender", e);
085      }
086   }
087
088   @Override
089   public void append(AppendParameter appendParameter) {
090      InputStream inputPdf = appendParameter.inputPdf();
091      try {
092         PDDocument doc = PDDocument.load(inputPdf);
093         setMetadata(doc, appendParameter);
094         attachZugferdFile(doc, appendParameter.attachmentFile());
095         doc.getDocument().setVersion(1.7f);
096         doc.save(appendParameter.resultingPdf());
097         doc.close();
098      } catch (Exception e) {
099         throw new InvoiceAppendError("Error appending Invoice", e);
100      }
101
102   }
103
104   private static void attachZugferdFile(PDDocument doc, InputStream zugferdFile) throws IOException {
105      PDEmbeddedFilesNameTreeNode fileNameTreeNode = new PDEmbeddedFilesNameTreeNode();
106
107      PDEmbeddedFile embeddedFile = createEmbeddedFile(doc, zugferdFile);
108      PDComplexFileSpecification fileSpecification = createFileSpecification(embeddedFile);
109
110      COSDictionary dict = fileSpecification.getCOSDictionary();
111      dict.setName("AFRelationship", "Alternative");
112      dict.setString("UF", ZF_FILE_NAME);
113
114      fileNameTreeNode.setNames(singletonMap(ZF_FILE_NAME, fileSpecification));
115
116      setNamesDictionary(doc, fileNameTreeNode);
117
118      COSArray cosArray = new COSArray();
119      cosArray.add(fileSpecification);
120      doc.getDocumentCatalog().getCOSDictionary().setItem("AF", cosArray);
121   }
122
123   private static PDComplexFileSpecification createFileSpecification(PDEmbeddedFile embeddedFile) {
124      PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
125      fileSpecification.setFile(ZF_FILE_NAME);
126      fileSpecification.setEmbeddedFile(embeddedFile);
127      return fileSpecification;
128   }
129
130   private static PDEmbeddedFile createEmbeddedFile(PDDocument doc, InputStream zugferdFile) throws IOException {
131      Calendar now = Calendar.getInstance();
132      ByteCountingInputStream countingIs = new ByteCountingInputStream(zugferdFile);
133      PDEmbeddedFile embeddedFile = new PDEmbeddedFile(doc, countingIs);
134      embeddedFile.setSubtype(MIME_TYPE);
135      embeddedFile.setSize(countingIs.getByteCount());
136      embeddedFile.setCreationDate(now);
137      embeddedFile.setModDate(now);
138      return embeddedFile;
139   }
140
141   private static void setNamesDictionary(PDDocument doc, PDEmbeddedFilesNameTreeNode fileNameTreeNode) {
142      PDDocumentCatalog documentCatalog = doc.getDocumentCatalog();
143      PDDocumentNameDictionary namesDictionary = new PDDocumentNameDictionary(documentCatalog);
144      namesDictionary.setEmbeddedFiles(fileNameTreeNode);
145      documentCatalog.setNames(namesDictionary);
146   }
147
148   private void setMetadata(PDDocument doc, AppendParameter appendParameter) throws IOException,
149         TransformerException, BadFieldValueException, XmpSerializationException {
150      Calendar now = Calendar.getInstance();
151      PDDocumentCatalog catalog = doc.getDocumentCatalog();
152      
153      PDMetadata metadata = new PDMetadata(doc);
154      catalog.setMetadata(metadata);
155      
156      XMPMetadata xmp = XMPMetadata.createXMPMetadata();
157      PDFAIdentificationSchema pdfaid = new PDFAIdentificationSchema(xmp);
158      pdfaid.setPart(Integer.valueOf(3)); 
159      pdfaid.setConformance("B");
160      xmp.addSchema(pdfaid);
161
162      DublinCoreSchema dublicCore = new DublinCoreSchema(xmp);
163      xmp.addSchema(dublicCore);
164
165      XMPBasicSchema basicSchema = new XMPBasicSchema(xmp);
166      basicSchema.setCreatorTool(PRODUCER);
167      basicSchema.setCreateDate(now);
168      xmp.addSchema(basicSchema);
169
170      PDDocumentInformation pdi = doc.getDocumentInformation();
171      pdi.setModificationDate(now);
172      pdi.setProducer(PRODUCER);
173      pdi.setAuthor(getAuthor());
174      doc.setDocumentInformation(pdi);
175
176      AdobePDFSchema pdf = new AdobePDFSchema(xmp);
177      pdf.setProducer(PRODUCER);
178      xmp.addSchema(pdf);
179
180      PDMarkInfo markinfo = new PDMarkInfo();
181      markinfo.setMarked(true);
182      doc.getDocumentCatalog().setMarkInfo(markinfo);
183
184      xmp.addSchema(zfDefaultXmp.getPDFExtensionSchema());
185      XMPSchemaZugferd1p0 zf = new XMPSchemaZugferd1p0(xmp);
186      zf.setConformanceLevel(appendParameter.zugferdConformanceLevel());
187      zf.setVersion(appendParameter.zugferdVersion());
188      xmp.addSchema(zf);
189
190      new XmpSerializer().serialize(xmp, metadata.createOutputStream(), true);
191   }
192
193   private static String getAuthor() {
194      return System.getProperty("user.name");
195   }
196
197}