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}