diff --git a/History.md b/History.md index b9eeb634..e09387fa 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +======= + +- issue #255 support order-x + + 2.3.4 ======= diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA1.java b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA1.java new file mode 100644 index 00000000..8ba61283 --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA1.java @@ -0,0 +1,124 @@ +/** ********************************************************************** + * + * Copyright 2020 Jochen Staerk + * + * Use is subject to license terms. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + *********************************************************************** */ +package org.mustangproject.ZUGFeRD; + + +import org.apache.pdfbox.preflight.PreflightDocument; +import org.apache.pdfbox.preflight.exception.ValidationException; +import org.apache.pdfbox.preflight.parser.PreflightParser; + +import javax.activation.DataSource; +import java.io.IOException; +import java.io.InputStream; + +public class OXExporterFromA1 extends OXExporterFromA3 implements IZUGFeRDExporter { + protected boolean ignorePDFAErrors = false; + + public OXExporterFromA1 ignorePDFAErrors() { + this.ignorePDFAErrors = true; + return this; + } + + private static boolean isValidA1(DataSource dataSource) throws IOException { + return getPDFAParserValidationResult(new PreflightParser(dataSource)); + } + + private static boolean getPDFAParserValidationResult(PreflightParser parser) throws IOException { + /* + * Parse the PDF file with PreflightParser that inherits from the + * NonSequentialParser. Some additional controls are present to check a set of + * PDF/A requirements. (Stream length consistency, EOL after some Keyword...) + */ + parser.parse();// might add a Format.PDF_A1A as parameter and iterate through A1 and A3 + + try (PreflightDocument document = parser.getPreflightDocument()) { + /* + * Once the syntax validation is done, the parser can provide a + * PreflightDocument (that inherits from PDDocument) This document process the + * end of PDF/A validation. + */ + + document.validate(); + + // Get validation result + return document.getResult().isValid(); + } catch (ValidationException e) { + /* + * the parse method can throw a SyntaxValidationException if the PDF file can't + * be parsed. In this case, the exception contains an instance of + * ValidationResult + */ + return false; + } + } + + + public OXExporterFromA1 setProfile(Profile p) { + return (OXExporterFromA1)super.setProfile(p); + } + public OXExporterFromA1 setProfile(String profileName) { + return (OXExporterFromA1)super.setProfile(profileName); + } + + public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException { + if (!ignorePDFAErrors && !isValidA1(dataSource)) { + throw new IOException("File is not a valid PDF/A input file"); + } + return true; + } + + public OXExporterFromA1() { + setZUGFeRDVersion(ZUGFeRDExporterFromA3.DefaultZUGFeRDVersion); + + } + + public OXExporterFromA1 load(String pdfFilename) throws IOException { + return (OXExporterFromA1) super.load(pdfFilename); + } + public OXExporterFromA1 load(byte[] pdfBinary) throws IOException { + return (OXExporterFromA1) super.load(pdfBinary); + } + public OXExporterFromA1 load(InputStream pdfSource) throws IOException{ + return (OXExporterFromA1) super.load(pdfSource); + } + public OXExporterFromA1 setCreator(String creator) { + return (OXExporterFromA1) super.setCreator(creator); + } + public OXExporterFromA1 setConformanceLevel(PDFAConformanceLevel newLevel) { + return (OXExporterFromA1) super.setConformanceLevel(newLevel); + } + public OXExporterFromA1 setProducer(String producer){ + return (OXExporterFromA1) super.setProducer(producer); + } + public OXExporterFromA1 setZUGFeRDVersion(int version){ + return (OXExporterFromA1) super.setZUGFeRDVersion(version); + } + public OXExporterFromA1 setXML(byte[] zugferdData) throws IOException{ + return (OXExporterFromA1) super.setXML(zugferdData); + } + + public OXExporterFromA1 disableAutoClose(boolean disableAutoClose){ + return (OXExporterFromA1) super.disableAutoClose(disableAutoClose); + } + public OXExporterFromA1 convertOnly() { + setAttachZUGFeRDHeaders(false); + return this; + } + +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA3.java new file mode 100644 index 00000000..f2d8ba5d --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/OXExporterFromA3.java @@ -0,0 +1,659 @@ +/** + * ********************************************************************* + *

+ * Copyright 2020 Jochen Staerk + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject.ZUGFeRD; + +import org.apache.pdfbox.cos.*; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.pdmodel.*; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; +import org.apache.pdfbox.pdmodel.font.PDCIDFontType2; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent; +import org.apache.pdfbox.preflight.utils.ByteArrayDataSource; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.AdobePDFSchema; +import org.apache.xmpbox.schema.DublinCoreSchema; +import org.apache.xmpbox.schema.PDFAIdentificationSchema; +import org.apache.xmpbox.schema.XMPBasicSchema; +import org.apache.xmpbox.type.ArrayProperty; +import org.apache.xmpbox.type.BadFieldValueException; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpParsingException; +import org.apache.xmpbox.xml.XmpSerializer; +import org.mustangproject.FileAttachment; + +import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.xml.transform.TransformerException; +import java.io.*; +import java.util.*; + +public class OXExporterFromA3 extends ZUGFeRDExporterFromA3 { + + protected PDFAConformanceLevel conformanceLevel = PDFAConformanceLevel.UNICODE; + protected ArrayList fileAttachments = new ArrayList(); + + /** + * This flag controls whether or not the metadata is overwritten, or kind of merged. + * The merging probably needs to be overhauled, but for my purpose it was good enough. + */ + protected boolean overwrite = true; + + private boolean disableAutoClose; + private boolean fileAttached = false; + private Profile profile = null; + private boolean documentPrepared = false; + + /** + * Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set, + * in which case passing a IZUGFeRDExportableTransaction is not necessary. By + * default it is null meaning the caller needs to pass a + * IZUGFeRDExportableTransaction for the XML to be populated. + */ + protected PDMetadata metadata = null; + /** + * Producer attribute for PDF + */ + protected String producer = "mustangproject"; + /** + * Author/Creator attribute for PDF + */ + protected String creator = "mustangproject"; + /** + * CreatorTool + */ + protected String creatorTool = "mustangproject"; + + /** + * @deprecated author is never set yet + */ + @Deprecated + protected String author; + /** + * @deprecated title is never set yet + */ + @Deprecated + protected String title; + /** + * @deprecated subject is never set yet + */ + @Deprecated + protected String subject; + + + private HashMap additionalXMLs = new HashMap(); + + + private boolean attachZUGFeRDHeaders = true; + + /** + * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the + * metadata level, this will not e.g. convert graphics to JPG-2000) + * + * @param pdfFilename filename of an PDF/A1 compliant document + */ + public OXExporterFromA3 load(String pdfFilename) throws IOException { + + ensurePDFIsValid(new FileDataSource(pdfFilename)); + try (FileInputStream pdf = new FileInputStream(pdfFilename)) { + return load(readAllBytes(pdf)); + } + } + + public IXMLProvider getProvider() { + return xmlProvider; + } + + public OXExporterFromA3 setProfile(Profile p) { + this.profile = p; + if (xmlProvider != null) { + xmlProvider.setProfile(p); + } + return this; + } + + public OXExporterFromA3 setProfile(String profilename) { + this.profile = Profiles.getByName(profilename); + + if (xmlProvider != null) { + xmlProvider.setProfile(this.profile); + } + return this; + } + + public OXExporterFromA3 addAdditionalFile(String name, byte[] content) { + fileAttachments.add(new FileAttachment(name, "text/xml", "Supplement", content).setDescription("ZUGFeRD extension/additional data")); + return this; + } + + + + /** + * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the + * metadata level, this will not e.g. convert graphics to JPG-2000) + * + * @param pdfBinary binary of a PDF/A1 compliant document + */ + public OXExporterFromA3 load(byte[] pdfBinary) throws IOException { + ensurePDFIsValid(new ByteArrayDataSource(new ByteArrayInputStream(pdfBinary))); + doc = PDDocument.load(pdfBinary); + return this; + } + + public OXExporterFromA3() { + super(); + } + + public void attachFile(FileAttachment file) { + fileAttachments.add(file); + } + + public void attachFile(String filename, byte[] data, String mimetype, String relation) { + FileAttachment fa = new FileAttachment(filename, mimetype, relation, data); + fileAttachments.add(fa); + } + + /*** + * Perform the final export to a now ZUGFeRD-enriched PDF file + * @param ZUGFeRDfilename the pdf file name + * @throws IOException if anything is wrong in the target location + */ + public void export(String ZUGFeRDfilename) throws IOException { + if (!documentPrepared) { + prepareDocument(); + } + if ((!fileAttached) && (attachZUGFeRDHeaders)) { + throw new IOException( + "File must be attached (usually with setTransaction) before perfoming this operation"); + } + doc.save(ZUGFeRDfilename); + if (!disableAutoClose) { + close(); + } + } + + @Override + public void close() throws IOException { + if (doc != null) { + doc.close(); + } + } + + + /*** + * Perform the final export to a now ZUGFeRD-enriched PDF file as OutputStream + * @param output the OutputStream + * @throws IOException if anything is wrong in the OutputStream + */ + public void export(OutputStream output) throws IOException { + if (!documentPrepared) { + prepareDocument(); + } + if ((!fileAttached) && (attachZUGFeRDHeaders)) { + throw new IOException( + "File must be attached (usually with setTransaction) before perfoming this operation"); + } + doc.save(output); + if (!disableAutoClose) { + close(); + } + } + + /** + * Embeds an external file (generic - any type allowed) in the PDF. + * + * @param doc PDDocument to attach the file to. + * @param filename name of the file that will become attachment name in the PDF + * @param relationship how the file relates to the content, e.g. "Alternative" + * @param description Human-readable description of the file content + * @param subType type of the data e.g. could be "text/xml" - mime like + * @param data the binary data of the file/attachment + * @throws IOException if anything is wrong with filename + */ + public void PDFAttachGenericFile(PDDocument doc, String filename, String relationship, String description, + String subType, byte[] data) throws IOException { + fileAttached = true; + + PDComplexFileSpecification fs = new PDComplexFileSpecification(); + fs.setFile(filename); + + COSDictionary dict = fs.getCOSObject(); + dict.setName("AFRelationship", relationship); + dict.setString("UF", filename); + dict.setString("Desc", description); + + ByteArrayInputStream fakeFile = new ByteArrayInputStream(data); + PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); +// ef.addCompression(); + ef.setSubtype(subType); + ef.setSize(data.length); + ef.setCreationDate(new GregorianCalendar()); + + ef.setModDate(GregorianCalendar.getInstance()); + + fs.setEmbeddedFile(ef); + + // In addition make sure the embedded file is set under /UF + dict = fs.getCOSObject(); + COSDictionary efDict = (COSDictionary) dict.getDictionaryObject(COSName.EF); + COSBase lowerLevelFile = efDict.getItem(COSName.F); + efDict.setItem(COSName.UF, lowerLevelFile); + + // now add the entry to the embedded file tree and set in the document. + PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog()); + PDEmbeddedFilesNameTreeNode efTree = names.getEmbeddedFiles(); + if (efTree == null) { + efTree = new PDEmbeddedFilesNameTreeNode(); + } + + Map namesMap = new HashMap<>(); + + Map oldNamesMap = efTree.getNames(); + if (oldNamesMap != null) { + for (String key : oldNamesMap.keySet()) { + namesMap.put(key, oldNamesMap.get(key)); + } + } + namesMap.put(filename, fs); + efTree.setNames(namesMap); + + names.setEmbeddedFiles(efTree); + doc.getDocumentCatalog().setNames(names); + + // AF entry (Array) in catalog with the FileSpec + COSBase AFEntry = (COSBase) doc.getDocumentCatalog().getCOSObject().getItem("AF"); + if ((AFEntry == null)) { + COSArray cosArray = new COSArray(); + cosArray.add(fs); + doc.getDocumentCatalog().getCOSObject().setItem("AF", cosArray); + } else if (AFEntry instanceof COSArray) { + COSArray cosArray = (COSArray) AFEntry; + cosArray.add(fs); + doc.getDocumentCatalog().getCOSObject().setItem("AF", cosArray); + } else if ((AFEntry instanceof COSObject) && + ((COSObject) AFEntry).getObject() instanceof COSArray) { + COSArray cosArray = (COSArray) ((COSObject) AFEntry).getObject(); + cosArray.add(fs); + } else { + throw new IOException("Unexpected object type for PDFDocument/Catalog/COSDictionary/Item(AF)"); + } + } + + /** + * Sets the ZUGFeRD XML data to be attached as a single byte array. This is + * useful for use-cases where the XML has already been produced by some external + * API or component. + * + * @param zugferdData XML data to be set as a byte array (XML file in raw form). + * @throws IOException (should not happen) + */ + public OXExporterFromA3 setXML(byte[] zugferdData) throws IOException { + CustomXMLProvider cus = new CustomXMLProvider(); + cus.setXML(zugferdData); + this.setXMLProvider(cus); + prepare(); + return this; + } + + + /** + * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the + * metadata level, this will not e.g. convert graphics to JPG-2000) + * + * @param pdfSource source to read a PDF/A1 compliant document from + */ + public OXExporterFromA3 load(InputStream pdfSource) throws IOException { + return load(readAllBytes(pdfSource)); + } + + public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException { + return true; + } + + private static byte[] readAllBytes(InputStream in) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + IOUtils.copy(in, buffer); + return buffer.toByteArray(); + } + + + /** + * All files are PDF/A-3, setConformance refers to the level conformance. + *

+ * PDF/A-3 has three coformance levels, called "A", "U" and "B". + *

+ * PDF/A-3-B where B means only visually preservable, U -standard for Mustang- + * means visually and unicode preservable and A means full compliance, i.e. + * visually, unicode and structurally preservable and tagged PDF, i.e. useful + * metainformation for blind people. + *

+ * Feel free to pass "A" as new level if you know what you are doing :-) + */ + public OXExporterFromA3 setConformanceLevel(PDFAConformanceLevel newLevel) { + conformanceLevel = newLevel; + return this; + } + + + public OXExporterFromA3 setCreator(String creator) { + this.creator = creator; + return this; + } + + public OXExporterFromA3 setCreatorTool(String creatorTool) { + this.creatorTool = creatorTool; + return this; + } + + public OXExporterFromA3 setProducer(String producer) { + this.producer = producer; + return this; + } + + protected OXExporterFromA3 setAttachZUGFeRDHeaders(boolean attachHeaders) { + this.attachZUGFeRDHeaders = attachHeaders; + return this; + } + + + /** + * This will add both the RDF-indication which embedded file is Zugferd and the + * neccessary PDF/A schema extension description to be able to add this + * information to RDF + * + * @param metadata the PDFbox XMPMetadata object + */ + protected void addXMP(XMPMetadata metadata) { + + if (attachZUGFeRDHeaders) { + XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, 1, true, xmlProvider.getProfile(), + "urn:factur-x:pdfa:CrossIndustryDocument:1p0#", "fx", + "order-x.xml"); + + metadata.addSchema(zf); + } + + XMPSchemaPDFAExtensions pdfaex = new XMPSchemaPDFAExtensions(this, metadata, 1, attachZUGFeRDHeaders); + pdfaex.setZUGFeRDVersion(1); + metadata.addSchema(pdfaex); + } + + + /** + * Embeds the Zugferd XML structure in a file named ZUGFeRD-invoice.xml. + * + * @param trans a IZUGFeRDExportableTransaction that provides the data-model to + * populate the XML. This parameter may be null, if so the XML data + * should hav ebeen set via + * setZUGFeRDXMLData(byte[] zugferdData) + * @throws IOException if anything is wrong with already loaded PDF + */ + public IExporter setTransaction(IExportableTransaction trans) throws IOException { + this.trans = trans; + return prepare(); + } + + public IExporter prepare() throws IOException { + prepareDocument(); + xmlProvider.generateXML(trans); + String filename = "order-x.xml"; + PDFAttachGenericFile(doc, filename, "Alternative", + "Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)", + "text/xml", xmlProvider.getXML()); + + for (FileAttachment attachment : fileAttachments) { + PDFAttachGenericFile(doc, attachment.getFilename(), attachment.getRelation(), attachment.getDescription(), attachment.getMimetype(), attachment.getData()); + } + + return this; + } + + /** + * Reads the XMPMetadata from the PDDocument, if it exists. + * Otherwise creates XMPMetadata. + */ + protected XMPMetadata getXmpMetadata() { + PDMetadata meta = doc.getDocumentCatalog().getMetadata(); + if (meta != null) { + try { + DomXmpParser xmpParser = new DomXmpParser(); + return xmpParser.parse(meta.toByteArray()); + } catch (XmpParsingException | IOException e) { + // TODO use logging or handle the error somehow + } + } + return XMPMetadata.createXMPMetadata(); + } + + protected byte[] serializeXmpMetadata(XMPMetadata xmpMetadata) throws TransformerException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMetadata, buffer, true); // see https://github.com/ZUGFeRD/mustangproject/issues/44 + return buffer.toByteArray(); + } + + /** + * Sets the producer if the overwrite flag is set or the producer is not already set. + * Sets the PDFVersion to 1.4 if the field is empty. + */ + protected void writeAdobePDFSchema(XMPMetadata xmp) { + AdobePDFSchema pdf = getAdobePDFSchema(xmp); + if (overwrite || isEmpty(pdf.getProducer())) + pdf.setProducer(producer); + } + + /** + * Returns the AdobePDFSchema from the XMPMetadata if it exists. + * If the overwrite flag is set or no AdobePDFSchema exists in the XMPMetadata, it is created, added and returned. + */ + protected AdobePDFSchema getAdobePDFSchema(XMPMetadata xmp) { + AdobePDFSchema pdf = xmp.getAdobePDFSchema(); + if (pdf != null) + if (overwrite) + xmp.removeSchema(pdf); + else + return pdf; + return xmp.createAndAddAdobePDFSchema(); + } + + protected void writePDFAIdentificationSchema(XMPMetadata xmp) { + PDFAIdentificationSchema pdfaid = getPDFAIdentificationSchema(xmp); + if (overwrite || isEmpty(pdfaid.getConformance())) { + try { + pdfaid.setConformance(conformanceLevel.getLetter()); + } catch (BadFieldValueException ex) { + // This should be impossible, because it would occur only if an illegal + // conformance level is supplied, + // however the enum enforces that the conformance level is valid. + throw new Error(ex); + } + } + pdfaid.setPart(3); + } + + protected PDFAIdentificationSchema getPDFAIdentificationSchema(XMPMetadata xmp) { + PDFAIdentificationSchema pdfaid = xmp.getPDFIdentificationSchema(); + if (pdfaid != null) + if (overwrite) + xmp.removeSchema(pdfaid); + else + return pdfaid; + return xmp.createAndAddPFAIdentificationSchema(); + } + + protected void writeDublinCoreSchema(XMPMetadata xmp) { + DublinCoreSchema dc = getDublinCoreSchema(xmp); + if (dc.getFormat() == null) + dc.setFormat("application/pdf"); + if ((overwrite || dc.getCreators() == null || dc.getCreators().isEmpty()) && creator != null) + dc.addCreator(creator); + if ((overwrite || dc.getDates() == null || dc.getDates().isEmpty()) && creator != null) + dc.addDate(Calendar.getInstance()); + + ArrayProperty titleProperty = dc.getTitleProperty(); + if (titleProperty != null) { + if (overwrite && !isEmpty(title)) { + dc.removeProperty(titleProperty); + dc.setTitle(title); + } else if (titleProperty.getElementsAsString().stream().anyMatch("Untitled"::equalsIgnoreCase)) { + // remove unfitting ghostscript default + dc.removeProperty(titleProperty); + } + } else if (!isEmpty(title)) { + dc.setTitle(title); + } + } + + protected DublinCoreSchema getDublinCoreSchema(XMPMetadata xmp) { + DublinCoreSchema dc = xmp.getDublinCoreSchema(); + if (dc != null) + if (overwrite) + xmp.removeSchema(dc); + else + return dc; + return xmp.createAndAddDublinCoreSchema(); + } + + protected void writeXMLBasicSchema(XMPMetadata xmp) { + XMPBasicSchema xsb = getXmpBasicSchema(xmp); + if (overwrite || isEmpty(xsb.getCreatorTool()) || "UnknownApplication".equals(xsb.getCreatorTool())) + xsb.setCreatorTool(creatorTool); + if (overwrite || xsb.getCreateDate() == null) + xsb.setCreateDate(GregorianCalendar.getInstance()); + } + + protected XMPBasicSchema getXmpBasicSchema(XMPMetadata xmp) { + XMPBasicSchema xsb = xmp.getXMPBasicSchema(); + if (xsb != null) + if (overwrite) + xmp.removeSchema(xsb); + else + return xsb; + return xmp.createAndAddXMPBasicSchema(); + } + + protected void writeDocumentInformation() { + String fullProducer = producer + " (via mustangproject.org " + Version.VERSION + ")"; + PDDocumentInformation info = doc.getDocumentInformation(); + if (overwrite || info.getCreationDate() == null) + info.setCreationDate(Calendar.getInstance()); + if (overwrite || info.getModificationDate() == null) + info.setModificationDate(Calendar.getInstance()); + if (overwrite || (isEmpty(info.getAuthor()) && !isEmpty(author))) + info.setAuthor(author); + if (overwrite || (isEmpty(info.getProducer()) && !isEmpty(fullProducer))) + info.setProducer(fullProducer); + if (overwrite || (isEmpty(info.getCreator()) && !isEmpty(creator))) + info.setCreator(creator); + if (overwrite || (isEmpty(info.getTitle()) && !isEmpty(title))) + info.setTitle(title); + if (overwrite || (isEmpty(info.getSubject()) && !isEmpty(subject))) + info.setSubject(subject); + } + + /** + * Adds an OutputIntent and the sRGB color profile if no OutputIntent exist + */ + protected void addSRGBOutputIntend() { + if (!doc.getDocumentCatalog().getOutputIntents().isEmpty()) { + return; + } + + try { + InputStream colorProfile = Thread.currentThread().getContextClassLoader().getResourceAsStream("sRGB.icc"); + if (colorProfile != null) { + PDOutputIntent intent = new PDOutputIntent(doc, colorProfile); + intent.setInfo("sRGB IEC61966-2.1"); + intent.setOutputCondition("sRGB IEC61966-2.1"); + intent.setOutputConditionIdentifier("sRGB IEC61966-2.1"); + intent.setRegistryName("http://www.color.org"); + doc.getDocumentCatalog().addOutputIntent(intent); + } + } catch (IOException e) { + // TODO use logging or handle the error somehow + } + } + + /** + * Adds a MarkInfo element to the PDF if it doesn't already exist and sets it as marked. + */ + protected void setMarked() { + PDDocumentCatalog catalog = doc.getDocumentCatalog(); + if (catalog.getMarkInfo() == null) { + catalog.setMarkInfo(new PDMarkInfo(doc.getPages().getCOSObject())); + } + catalog.getMarkInfo().setMarked(true); + } + + /** + * Adds a StructureTreeRoot element to the PDF if it doesn't already exist. + */ + protected void addStructureTreeRoot() { + if (doc.getDocumentCatalog().getStructureTreeRoot() == null) { + doc.getDocumentCatalog().setStructureTreeRoot(new PDStructureTreeRoot()); + } + } + + + /** + * @return if pdf file will be automatically closed after adding ZF + */ + public boolean isAutoCloseDisabled() { + return disableAutoClose; + } + + /** + * @param disableAutoClose prevent PDF file from being closed after adding ZF + */ + public OXExporterFromA3 disableAutoClose(boolean disableAutoClose) { + this.disableAutoClose = disableAutoClose; + return this; + } + + protected void setXMLProvider(IXMLProvider p) { + this.xmlProvider = p; + if (profile != null) { + xmlProvider.setProfile(profile); + } + } + + @Override + public OXExporterFromA3 setZUGFeRDVersion(int version) { + OXPullProvider z2p = new OXPullProvider(); + setXMLProvider(z2p); + return this; + } + + /** + * Utility method inspired by apache commons-lang3 StringUtils. + * + * @param string the string to test + * @return true if the string is null or empty + */ + private boolean isEmpty(String string) { + return string == null || string.isEmpty(); + } +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java b/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java index c8eadb32..bb3e3a75 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java @@ -40,10 +40,8 @@ public class XMPSchemaZugferd extends XMPSchema { * @param URN the xml URI for the XMP * @param prefix the xml namespace prefix for the XMP, zf for ZUGFeRD, fx for Factur-X * @param filename the filename of the invoice - * @param version meta-data version to set. May be null, and then it is set automatically */ - public XMPSchemaZugferd(XMPMetadata metadata, int zfVersion, boolean isFacturX, Profile conformanceLevel, - String URN, String prefix, String filename, String version) { + public XMPSchemaZugferd(XMPMetadata metadata, int zfVersion, boolean isFacturX, Profile conformanceLevel, String URN, String prefix, String filename) { super(metadata, URN, prefix, "ZUGFeRD Schema"); setAboutAsSimple(""); @@ -52,12 +50,9 @@ public class XMPSchemaZugferd extends XMPSchema { setTextPropertyValue("ConformanceLevel", conformanceLevelValue); setTextPropertyValue("DocumentType", "INVOICE"); setTextPropertyValue("DocumentFileName", filename); - - if (version == null) { - version = "1.0"; - if ((zfVersion==2)&&(!isFacturX)) { - version="2p0"; - } + String version="1.0"; + if ((zfVersion==2)&&(!isFacturX)) { + version="2p0"; } setTextPropertyValue("Version", version); } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java index 242f8a7e..0ab4f772 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD1PullProvider.java @@ -57,19 +57,19 @@ public class ZUGFeRD1PullProvider extends ZUGFeRD2PullProvider implements IXMLPr public void setTest() { } - private String vatFormat(BigDecimal value) { + protected String vatFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String currencyFormat(BigDecimal value) { + protected String currencyFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String priceFormat(BigDecimal value) { + protected String priceFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } - private String quantityFormat(BigDecimal value) { + protected String quantityFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index b283d497..c4be4561 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -59,19 +59,19 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { public void setTest() { } - private String vatFormat(BigDecimal value) { + protected String vatFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String currencyFormat(BigDecimal value) { + protected String currencyFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 2); } - private String priceFormat(BigDecimal value) { + protected String priceFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } - private String quantityFormat(BigDecimal value) { + protected String quantityFormat(BigDecimal value) { return XMLTools.nDigitFormat(value, 4); } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java index 6106215f..8aff3bbe 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java @@ -69,7 +69,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte private boolean disableAutoClose; private boolean fileAttached = false; private Profile profile = null; - private boolean documentPrepared = false; + protected boolean documentPrepared = false; /** * Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set, @@ -116,11 +116,6 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte protected int ZFVersion = DefaultZUGFeRDVersion; private boolean attachZUGFeRDHeaders = true; - // Specific metaData version in case of XRechnung. We need it to be settable - // by the caller if necessary. - protected String XRechnungVersion = null; // Default XRechnung as of late 2021 is 2p0 - - /** * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the * metadata level, this will not e.g. convert graphics to JPG-2000) @@ -167,16 +162,13 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * @return the URN of the namespace */ public String getNamespaceForVersion(int ver) { - // In the case of XRechnung, it is the same as Factur-X - if ((ver >= 2) && (this.profile != null) && - this.profile == Profiles.getByName("XRECHNUNG")) { - return "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"; - } else if (isFacturX) { + if (isFacturX) { return "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"; } else if (ver == 1) { return "urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#"; } else if (ver == 2) { return "urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#"; + } else { throw new IllegalArgumentException("Version not supported"); } @@ -188,18 +180,13 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * @return the namespace prefix as string, without colon */ public String getPrefixForVersion(int ver) { - // In the case of XRechnung, it is the same as Factur-X - if ((this.profile != null) && - this.profile == Profiles.getByName("XRECHNUNG")) { - return "fx"; - } else if (isFacturX) { + if (isFacturX) { return "fx"; } else { return "zf"; } } - /*** * internal helper: return the name of the file attachment for the given zf/fx version * @param ver the zf/fx version @@ -207,19 +194,17 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * @return the filename of the file to be embedded */ public String getFilenameForVersion(int ver, Profile profile) { - boolean isXRechnung = (this.profile != null) && - this.profile == Profiles.getByName("XRECHNUNG"); - if (isFacturX) { return "factur-x.xml"; - } else if (isXRechnung) { - return "xrechnung.xml"; } else { - if (ver == 1) { return "ZUGFeRD-invoice.xml"; } else { - return "zugferd-invoice.xml"; + if (profile.getName().equals("XRECHNUNG")) { + return "xrechnung.xml"; + } else { + return "zugferd-invoice.xml"; + } } } } @@ -237,18 +222,6 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte return this; } - /** - * Sets a specific XRechnung version from outside. This version needs to be present in the - * meta-data as well. The caller may wish to generate a specific version of XRechnung - * depending on the time period etc. - * - * @param XRechnungVersion the XRechnung version - */ - public void setXRechnungSpecificVersion(String XRechnungVersion) { - this.XRechnungVersion = XRechnungVersion; - } - - /*** * Generate ZF2.0/2.1 files with filename zugferd-invoice.xml instead of factur-x.xml */ @@ -329,24 +302,6 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte } } - - /** - * Embeds an external file (generic - any type allowed) in the PDF. - * The embedding is done in the default PDF document. - * - * @param filename name of the file that will become attachment name in the PDF - * @param relationship how the file relates to the content, e.g. "Alternative" - * @param description Human-readable description of the file content - * @param subType type of the data e.g. could be "text/xml" - mime like - * @param data the binary data of the file/attachment - * @throws java.io.IOException if anything is wrong with filename - */ - public void PDFAttachGenericFile(String filename, String relationship, String description, - String subType, byte[] data) throws IOException { - PDFAttachGenericFile(this.doc, filename, relationship, description, subType, data); - } - - /** * Embeds an external file (generic - any type allowed) in the PDF. * @@ -513,18 +468,10 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte */ protected void addXMP(XMPMetadata metadata) { - String metaDataVersion = null; // default will be used - - // The XRechnung version may be settable from outside. - if ((this.XRechnungVersion != null) && (this.profile != null) && - this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName())) { - metaDataVersion = this.XRechnungVersion; - } - if (attachZUGFeRDHeaders) { XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, ZFVersion, isFacturX, xmlProvider.getProfile(), getNamespaceForVersion(ZFVersion), getPrefixForVersion(ZFVersion), - getFilenameForVersion(ZFVersion, xmlProvider.getProfile()), metaDataVersion); + getFilenameForVersion(ZFVersion, xmlProvider.getProfile())); metadata.addSchema(zf); } @@ -534,8 +481,8 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte metadata.addSchema(pdfaex); } - private void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) - throws IOException { + protected void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) { + // https://github.com/ZUGFeRD/mustangproject/issues/249 COSName cidSet = COSName.getPDFName("CIDSet"); @@ -561,7 +508,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte } } } catch (IOException e) { - throw e; + e.printStackTrace(); } // do stuff with the font } @@ -621,19 +568,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte prepareDocument(); xmlProvider.generateXML(trans); String filename = getFilenameForVersion(ZFVersion, xmlProvider.getProfile()); - - String relationship = "Alternative"; - // ZUGFeRD 2.1.1 Technical Supplement | Part A | 2.2.2. Data Relationship - // See documentation ZUGFeRD211_EN/Documentation/ZUGFeRD-2.1.1 - Specification_TA_Part-A.pdf - // https://www.ferd-net.de/standards/zugferd-2.1.1/index.html - if (ZFVersion >= 2) { - if (this.profile == Profiles.getByName("MINIMUM") || - this.profile == Profiles.getByName("BASICWL")) { - relationship = "Data"; - } - } - - PDFAttachGenericFile(doc, filename, relationship, + PDFAttachGenericFile(doc, filename, "Alternative", "Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)", "text/xml", xmlProvider.getXML()); @@ -648,15 +583,14 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * Reads the XMPMetadata from the PDDocument, if it exists. * Otherwise creates XMPMetadata. */ - protected XMPMetadata getXmpMetadata() - throws IOException { + protected XMPMetadata getXmpMetadata() { PDMetadata meta = doc.getDocumentCatalog().getMetadata(); - if ((meta != null) && (meta.getLength() > 0)) { + if (meta != null) { try { DomXmpParser xmpParser = new DomXmpParser(); return xmpParser.parse(meta.toByteArray()); - } catch (XmpParsingException e) { - throw new IOException(e); + } catch (XmpParsingException | IOException e) { + // TODO use logging or handle the error somehow } } return XMPMetadata.createXMPMetadata(); @@ -790,8 +724,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte /** * Adds an OutputIntent and the sRGB color profile if no OutputIntent exist */ - protected void addSRGBOutputIntend() - throws IOException { + protected void addSRGBOutputIntend() { if (!doc.getDocumentCatalog().getOutputIntents().isEmpty()) { return; } @@ -807,7 +740,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte doc.getDocumentCatalog().addOutputIntent(intent); } } catch (IOException e) { - throw e; + // TODO use logging or handle the error somehow } } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java index 6d9c2271..9e194504 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java @@ -572,7 +572,7 @@ public class ZUGFeRDImporter { public String getAmount() { String result = extractString("//*[local-name() = 'SpecifiedTradeSettlementHeaderMonetarySummation']/*[local-name() = 'DuePayableAmount']"); if (result == null || result.isEmpty()) { - result = extractString("//*[local-name() = 'SpecifiedTradeSettlementMonetarySummation']/*[local-name() = 'GrandTotalAmount']"); + result = extractString("//*[local-name() = 'SpecifiedTradeSettlementHeaderMonetarySummation']/*[local-name() = 'GrandTotalAmount']"); } return result; } diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java index a1ff5aee..bc261bed 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/OXTest.java @@ -280,18 +280,14 @@ public class OXTest extends MustangReaderTestCase implements IExportableTransact // now check the contents (like MustangReaderTest) ZUGFeRDImporter zi = new ZUGFeRDImporter(TARGET_PDF); - assertTrue(zi.getUTF8().contains("59")); + assertTrue(zi.getUTF8().contains("220")); assertTrue(zi.getUTF8().contains("")); - assertTrue(zi.getUTF8().contains("DE540815")); - assertTrue(zi.getUTF8().contains("DE99XX12345")); - assertFalse(zi.getUTF8().contains("")); assertFalse(zi.getUTF8().contains("EUR")); assertTrue(zi.getUTF8().contains("USD"));//currency should be USD, test for #150 // Reading ZUGFeRD assertEquals("496.00", zi.getAmount()); assertEquals(zi.getHolder(), getOwnOrganisationName()); - assertEquals(zi.getForeignReference(), getNumber()); }