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}