001/* 002 * Copyright (C) 2014 Konik.io 003 * 004 * This file is part of Konik library. 005 * 006 * Konik library is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU Affero General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Konik library is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU Affero General Public License for more details. 015 * 016 * You should have received a copy of the GNU Affero General Public License 017 * along with Konik library. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package io.konik.itext.appender; 020 021import static com.itextpdf.text.pdf.AFRelationshipValue.Alternative; 022import static com.itextpdf.text.pdf.PdfName.AFRELATIONSHIP; 023import static com.itextpdf.text.pdf.PdfName.MODDATE; 024import static com.itextpdf.text.pdf.PdfName.PARAMS; 025import io.konik.InvoiceTransformer; 026import io.konik.exception.TransformationWarning; 027import io.konik.harness.InvoiceAppendError; 028import io.konik.harness.InvoiceAppender; 029import io.konik.itext.xmp.XmpAppender; 030import io.konik.itext.xmp.ZfXmpInfo; 031import io.konik.zugferd.Invoice; 032import io.konik.zugferd.profile.Profile; 033 034import java.io.ByteArrayInputStream; 035import java.io.ByteArrayOutputStream; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.OutputStream; 039import java.util.logging.Logger; 040 041import javax.inject.Inject; 042import javax.inject.Named; 043import javax.inject.Singleton; 044 045import com.itextpdf.text.DocumentException; 046import com.itextpdf.text.pdf.PdfAConformanceLevel; 047import com.itextpdf.text.pdf.PdfAStamper; 048import com.itextpdf.text.pdf.PdfArray; 049import com.itextpdf.text.pdf.PdfDate; 050import com.itextpdf.text.pdf.PdfDictionary; 051import com.itextpdf.text.pdf.PdfFileSpecification; 052import com.itextpdf.text.pdf.PdfName; 053import com.itextpdf.text.pdf.PdfReader; 054 055/** 056 * The Class IText PDF Invoice Appender. 057 * 058 * For now we expect a compliant PDF/A-3B. 059 * 060 */ 061@Named 062@Singleton 063public class ITextPdfInvoiceAppender implements InvoiceAppender { 064 065 private static final String INVOICE = "INVOICE"; 066 067 private static final String ZF_FILE_NAME = "ZUGFeRD-invoice.xml"; 068 069 private final XmpAppender xmp; 070 071 private final InvoiceTransformer transformer; 072 073 /** 074 * Instantiates a new i-text pdf invoice appender. 075 */ 076 public ITextPdfInvoiceAppender() { 077 this(new XmpAppender(),new InvoiceTransformer()); 078 } 079 080 /** 081 * Instantiates a new i-text pdf invoice appender. 082 * 083 * @param xmpAppender the xmp appender 084 * @param invoiceTransformer the invoice transformer 085 */ 086 @Inject 087 public ITextPdfInvoiceAppender(XmpAppender xmpAppender, InvoiceTransformer invoiceTransformer) { 088 this.xmp = xmpAppender; 089 this.transformer = invoiceTransformer; 090 } 091 092 /** 093 * Append invoice. 094 * 095 * @param invoice the invoice 096 * @param pdf the in PDF byte array 097 * @return the byte[] 098 */ 099 @Override 100 public byte[] append(final Invoice invoice, final byte[] pdf) { 101 ByteArrayInputStream isPdf = new ByteArrayInputStream(pdf); 102 ByteArrayOutputStream osPdf = new ByteArrayOutputStream(pdf.length); 103 104 append(invoice, isPdf, osPdf); 105 106 return osPdf.toByteArray(); 107 } 108 109 /** 110 * Append invoice. 111 * 112 * @param invoice the invoice 113 * @param inputPdf the input pdf 114 * @param resultingPdf the resulting pdf 115 */ 116 @Override 117 public void append(final Invoice invoice, InputStream inputPdf, OutputStream resultingPdf) { 118 try { 119 appendInvoiceIntern(invoice, inputPdf, resultingPdf); 120 } catch (DocumentException e) { 121 throw new InvoiceAppendError("Could not open PD for modification or to close it", e); 122 } catch (IOException e) { 123 throw new InvoiceAppendError("PDF IO Error", e); 124 } 125 } 126 127 /** 128 * Append invoice intern. 129 * 130 * @param invoice the invoice 131 * @param inPdf the in pdf 132 * @param output the output 133 * @throws IOException Signals that an I/O exception has occurred. 134 * @throws DocumentException the document exception 135 */ 136 private void appendInvoiceIntern(Invoice invoice, InputStream inPdf, OutputStream output) throws IOException, 137 DocumentException { 138 139 byte[] content = transformer.from(invoice); 140 141 PdfReader reader = new PdfReader(inPdf); 142 143 PdfAStamper stamper = new PdfAStamper(reader, output, PdfAConformanceLevel.PDF_A_3B); 144 145 appendZfContentToXmp(stamper,invoice); 146 147 // Creating PDF/A-3 compliant attachment. 148 PdfDictionary embeddedFileParams = new PdfDictionary(); 149 embeddedFileParams.put(PARAMS, new PdfName(ZF_FILE_NAME)); 150 embeddedFileParams.put(MODDATE, new PdfDate()); 151 PdfFileSpecification fs = PdfFileSpecification.fileEmbedded(stamper.getWriter(), null, ZF_FILE_NAME, content, 152 "text/xml", embeddedFileParams, 0); 153 fs.put(AFRELATIONSHIP, Alternative); 154 stamper.addFileAttachment(ZF_FILE_NAME, fs); 155 156 //AF 157 PdfArray array = new PdfArray(); 158 array.add(fs.getReference()); 159 stamper.getWriter().getExtraCatalog().put(new PdfName("AF"), array); 160 161 stamper.close(); 162 reader.close(); 163 } 164 165 private void appendZfContentToXmp(PdfAStamper stamper, Invoice invoice) throws IOException { 166 Profile profile = invoice.getContext().getProfile(); 167 ZfXmpInfo info = new ZfXmpInfo(profile, ZF_FILE_NAME, INVOICE); 168 try { 169 byte[] newXmpMetadata = xmp.append(stamper.getReader().getMetadata(), info); 170 stamper.setXmpMetadata(newXmpMetadata); 171 } catch (TransformationWarning e) { 172 throw new InvoiceAppendError("Error Appending XMP to PDF", e); 173 // TODO if we don't rethrow we should provide a result object with a warning Msg. 174 } 175 } 176}