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}