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.itext;
019
020import static com.itextpdf.text.pdf.AFRelationshipValue.Alternative;
021import static com.itextpdf.text.pdf.PdfName.AFRELATIONSHIP;
022import static com.itextpdf.text.pdf.PdfName.MODDATE;
023import static com.itextpdf.text.pdf.PdfName.PARAMS;
024import io.konik.harness.AppendParameter;
025import io.konik.harness.FileAppender;
026import io.konik.harness.exception.InvoiceAppendError;
027
028import java.io.ByteArrayOutputStream;
029import java.io.IOException;
030import java.io.InputStream;
031
032import javax.inject.Named;
033import javax.inject.Singleton;
034
035import com.itextpdf.text.DocumentException;
036import com.itextpdf.text.pdf.PdfAConformanceLevel;
037import com.itextpdf.text.pdf.PdfAStamper;
038import com.itextpdf.text.pdf.PdfArray;
039import com.itextpdf.text.pdf.PdfDate;
040import com.itextpdf.text.pdf.PdfDictionary;
041import com.itextpdf.text.pdf.PdfFileSpecification;
042import com.itextpdf.text.pdf.PdfName;
043import com.itextpdf.text.pdf.PdfReader;
044import com.itextpdf.text.xml.xmp.XmpWriter;
045import com.itextpdf.xmp.XMPException;
046import com.itextpdf.xmp.XMPMeta;
047import com.itextpdf.xmp.XMPMetaFactory;
048import com.itextpdf.xmp.XMPUtils;
049
050/**
051 * The Class IText PDF Invoice Appender.
052 *
053 * For now we expect a compliant PDF/A-3B.
054 *
055 */
056@Named
057@Singleton
058public class ITextInvoiceAppender implements FileAppender {
059
060   private static final String MIME_TYPE = "text/xml";
061   private static final String ZF_FILE_NAME = "ZUGFeRD-invoice.xml";
062   private static final String ZF_NS = "urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#";
063
064   @Override
065   public void append(AppendParameter appendable) {
066      try {
067         appendInvoiceIntern(appendable);
068      } catch (DocumentException e) {
069         throw new InvoiceAppendError("Could not open PD for modification or to close it", e);
070      } catch (IOException e) {
071         throw new InvoiceAppendError("PDF IO Error", e);
072      } catch (XMPException e) {
073         throw new InvoiceAppendError("Error with XMP Extension", e);
074      }
075   }
076
077   /**
078    * Append invoice intern.
079    *
080    * @param appendable the appendable
081    * @throws IOException Signals that an I/O exception has occurred.
082    * @throws DocumentException the document exception
083    * @throws XMPException the XMP exception
084    */
085   private void appendInvoiceIntern(AppendParameter appendable) throws IOException, DocumentException, XMPException {
086      byte[] attachmentFile = convertToByteArray(appendable.attachmentFile());
087      PdfReader reader = new PdfReader(appendable.inputPdf());
088      PdfAStamper stamper = new PdfAStamper(reader, appendable.resultingPdf(), PdfAConformanceLevel.PDF_A_3B);
089
090      appendZfMetadata(stamper, appendable.zugferdConformanceLevel(), appendable.zugferdVersion());
091      attachFile(attachmentFile, stamper);
092
093      stamper.close();
094      reader.close();
095   }
096
097   private static void attachFile(byte[] attachmentFile, PdfAStamper stamper) throws IOException {
098      PdfDictionary embeddedFileParams = new PdfDictionary();
099      embeddedFileParams.put(PARAMS, new PdfName(ZF_FILE_NAME));
100      embeddedFileParams.put(MODDATE, new PdfDate());
101
102      PdfFileSpecification fs = PdfFileSpecification.fileEmbedded(stamper.getWriter(), null, ZF_FILE_NAME,
103            attachmentFile, MIME_TYPE, embeddedFileParams, 0);
104      fs.put(AFRELATIONSHIP, Alternative);
105      stamper.addFileAttachment(ZF_FILE_NAME, fs);
106
107      PdfArray array = new PdfArray();
108      array.add(fs.getReference());
109      stamper.getWriter().getExtraCatalog().put(new PdfName("AF"), array);
110   }
111
112   private void appendZfMetadata(PdfAStamper stamper, String conformanceLevel, String zfVersion) throws XMPException {
113      stamper.createXmpMetadata();
114      XmpWriter xmpWriter = stamper.getXmpWriter();
115      XMPMeta xmpMeta = xmpWriter.getXmpMeta();
116      InputStream zfExtensionIs = this.getClass().getResourceAsStream("/zf_extension.xmp");
117      XMPMeta zfExtensionMetadata = XMPMetaFactory.parse(zfExtensionIs);
118      XMPUtils.appendProperties(zfExtensionMetadata, xmpMeta, true, false);
119      xmpWriter.setProperty(ZF_NS, "ConformanceLevel", conformanceLevel);
120      xmpWriter.setProperty(ZF_NS, "Version", zfVersion);
121   }
122
123   private static byte[] convertToByteArray(InputStream is) {
124      ByteArrayOutputStream baos = new ByteArrayOutputStream();
125
126      byte[] buffer = new byte[65536];
127      try {
128         for (int length; (length = is.read(buffer)) != -1;) {
129            baos.write(buffer, 0, length);
130         }
131         is.close();
132         baos.close();
133      } catch (IOException e) {
134         throw new InvoiceAppendError("Was not possible to read Invoice Content stream", e);
135      }
136      return baos.toByteArray();
137   }
138
139}