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}