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.ConformanceLevel;
033
034import java.io.ByteArrayInputStream;
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.InputStream;
038import java.io.OutputStream;
039
040import javax.inject.Inject;
041import javax.inject.Named;
042import javax.inject.Singleton;
043
044import com.itextpdf.text.DocumentException;
045import com.itextpdf.text.pdf.PdfAConformanceLevel;
046import com.itextpdf.text.pdf.PdfAStamper;
047import com.itextpdf.text.pdf.PdfArray;
048import com.itextpdf.text.pdf.PdfDate;
049import com.itextpdf.text.pdf.PdfDictionary;
050import com.itextpdf.text.pdf.PdfFileSpecification;
051import com.itextpdf.text.pdf.PdfName;
052import com.itextpdf.text.pdf.PdfReader;
053
054/**
055 * The Class IText PDF Invoice Appender.
056 *
057 * For now we expect a compliant PDF/A-3B.
058 *
059 */
060@Named
061@Singleton
062public class ITextPdfInvoiceAppender implements InvoiceAppender {
063
064   private static final String INVOICE = "INVOICE";
065
066   private static final String ZF_FILE_NAME = "ZUGFeRD-invoice.xml";
067
068   private final XmpAppender xmp;
069
070   private final InvoiceTransformer transformer;
071
072   /**
073    * Instantiates a new i-text pdf invoice appender.
074    */
075   public ITextPdfInvoiceAppender() {
076      this(new XmpAppender(),new InvoiceTransformer());
077   }
078   
079   /**
080    * Instantiates a new i-text pdf invoice appender.
081    *
082    * @param xmpAppender the xmp appender
083    * @param invoiceTransformer the invoice transformer
084    */
085   @Inject
086   public ITextPdfInvoiceAppender(XmpAppender xmpAppender, InvoiceTransformer invoiceTransformer) {
087      this.xmp = xmpAppender;
088      this.transformer = invoiceTransformer;
089   }
090
091   /**
092    * Append invoice.
093    *
094    * @param invoice the invoice
095    * @param pdf the in PDF byte array
096    * @return the byte[]
097    */
098   @Override
099   public byte[] append(final Invoice invoice, final byte[] pdf) {
100      ByteArrayInputStream isPdf = new ByteArrayInputStream(pdf);
101      ByteArrayOutputStream osPdf = new ByteArrayOutputStream(pdf.length);
102
103      append(invoice, isPdf, osPdf);
104
105      return osPdf.toByteArray();
106   }
107
108   /**
109    * Append invoice.
110    *
111    * @param invoice the invoice
112    * @param inputPdf the input pdf
113    * @param resultingPdf the resulting pdf
114    */
115   @Override
116   public void append(final Invoice invoice, InputStream inputPdf, OutputStream resultingPdf) {
117      try {
118         appendInvoiceIntern(invoice, inputPdf, resultingPdf);
119      } catch (DocumentException e) {
120         throw new InvoiceAppendError("Could not open PD for modification or to close it", e);
121      } catch (IOException e) {
122         throw new InvoiceAppendError("PDF IO Error", e);
123      }
124   }
125
126   /**
127    * Append invoice intern.
128    *
129    * @param invoice the invoice
130    * @param inPdf the in pdf
131    * @param output the output
132    * @throws IOException Signals that an I/O exception has occurred.
133    * @throws DocumentException the document exception
134    */
135   private void appendInvoiceIntern(Invoice invoice, InputStream inPdf, OutputStream output) throws IOException,
136         DocumentException {
137
138      byte[] content = transformer.fromModel(invoice);
139
140      PdfReader reader = new PdfReader(inPdf);
141
142      PdfAStamper stamper = new PdfAStamper(reader, output, PdfAConformanceLevel.PDF_A_3B);
143
144      appendZfContentToXmp(stamper,invoice);
145
146      // Creating PDF/A-3 compliant attachment.
147      PdfDictionary embeddedFileParams = new PdfDictionary();
148      embeddedFileParams.put(PARAMS, new PdfName(ZF_FILE_NAME));
149      embeddedFileParams.put(MODDATE, new PdfDate());
150      PdfFileSpecification fs = PdfFileSpecification.fileEmbedded(stamper.getWriter(), null, ZF_FILE_NAME, content,
151            "text/xml", embeddedFileParams, 0);
152      fs.put(AFRELATIONSHIP, Alternative);
153      stamper.addFileAttachment(ZF_FILE_NAME, fs);
154
155      //AF
156      PdfArray array = new PdfArray();
157      array.add(fs.getReference());
158      stamper.getWriter().getExtraCatalog().put(new PdfName("AF"), array);
159
160      stamper.close();
161      reader.close();
162   }
163
164   private void appendZfContentToXmp(PdfAStamper stamper, Invoice invoice) throws IOException {
165      ConformanceLevel profile = invoice.getContext().getGuideline().getConformanceLevel();
166      ZfXmpInfo info = new ZfXmpInfo(profile, ZF_FILE_NAME, INVOICE);
167      try {
168         byte[] newXmpMetadata = xmp.append(stamper.getReader().getMetadata(), info);
169         stamper.setXmpMetadata(newXmpMetadata);
170      } catch (TransformationWarning e) {
171         throw new InvoiceAppendError("Error Appending XMP to PDF", e);
172         // TODO if we don't rethrow we should provide a result object with a warning Msg.
173      }
174   }
175
176   @Override
177   public byte[] append(Invoice invoice, InputStream inputStreamPdf) {
178      return null;
179   }
180}