From bb601dc8aa70faa53cdfac917161f7d2ef7fc1a3 Mon Sep 17 00:00:00 2001 From: Jeremias Maerki Date: Fri, 17 Feb 2006 16:18:04 +0000 Subject: [PATCH] XMP metadata generation from the PDFInfo object now conforms to PDF/A-1 (6.7.3 in ISO 19005-1:2005(E)). On PDFDocument, it is now possible to set the PDF-A mode which triggers conformance checks where necessary. The first PDF/A-1b conformance checks are now in the code. When conformance problems are detected, the new PDFConformanceException (subclass of RuntimeException since this would be a programmer error) is thrown. PDF Renderer got a setting (through rendering options and XML configuration) to set the PDF/A-1 mode. Default is "disabled". git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@378560 13f79535-47bb-0310-9956-ffa450edef68 --- .../fop/fo/extensions/xmp/XMPConstants.java | 3 + src/java/org/apache/fop/pdf/PDFAMode.java | 71 +++++++++++++++++++ .../fop/pdf/PDFConformanceException.java | 46 ++++++++++++ src/java/org/apache/fop/pdf/PDFDocument.java | 54 +++++++++++--- src/java/org/apache/fop/pdf/PDFInfo.java | 7 +- src/java/org/apache/fop/pdf/PDFMetadata.java | 32 ++++++++- .../apache/fop/render/pdf/PDFRenderer.java | 17 +++++ 7 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 src/java/org/apache/fop/pdf/PDFAMode.java create mode 100644 src/java/org/apache/fop/pdf/PDFConformanceException.java diff --git a/src/java/org/apache/fop/fo/extensions/xmp/XMPConstants.java b/src/java/org/apache/fop/fo/extensions/xmp/XMPConstants.java index b8b29988c..ed189aa38 100644 --- a/src/java/org/apache/fop/fo/extensions/xmp/XMPConstants.java +++ b/src/java/org/apache/fop/fo/extensions/xmp/XMPConstants.java @@ -35,4 +35,7 @@ public interface XMPConstants { /** Namespace URI for the XMP Basic Schema */ String XMP_BASIC_NAMESPACE = "http://ns.adobe.com/xap/1.0/"; + /** Namespace URI for the Adobe PDF Schema */ + String ADOBE_PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/"; + } diff --git a/src/java/org/apache/fop/pdf/PDFAMode.java b/src/java/org/apache/fop/pdf/PDFAMode.java new file mode 100644 index 000000000..52e1f02f5 --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFAMode.java @@ -0,0 +1,71 @@ +/* + * Copyright 2006 The Apache Software Foundation. + * + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +/** Enum class for PDF/A modes. */ +public final class PDFAMode { + + /** PDF/A disabled */ + public static final PDFAMode DISABLED = new PDFAMode("PDF/A disabled"); + /** PDF/A-1a enabled */ + public static final PDFAMode PDFA_1A = new PDFAMode("PDF/A-1a"); + /** PDF/A-1b enabled */ + public static final PDFAMode PDFA_1B = new PDFAMode("PDF/A-1b"); + + private String name; + + /** + * Constructor to add a new named item. + * @param name Name of the item. + */ + private PDFAMode(String name) { + this.name = name; + } + + /** @return the name of the enum */ + public String getName() { + return this.name; + } + + /** @return true if this mode obey the restrictions established by PDF/A-1b. */ + public boolean isPDFA1LevelB() { + return (this != DISABLED); + } + + /** + * Returns the mode enum object given a String. + * @param s the string + * @return the PDFAMode enum object (DISABLED will be returned if no match is found) + */ + public static PDFAMode valueOf(String s) { + if (PDFA_1A.getName().equalsIgnoreCase(s)) { + return PDFA_1A; + } else if (PDFA_1B.getName().equalsIgnoreCase(s)) { + return PDFA_1B; + } else { + return DISABLED; + } + } + + /** @see java.lang.Object#toString() */ + public String toString() { + return name; + } + +} diff --git a/src/java/org/apache/fop/pdf/PDFConformanceException.java b/src/java/org/apache/fop/pdf/PDFConformanceException.java new file mode 100644 index 000000000..b8f8e3015 --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFConformanceException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2006 The Apache Software Foundation. + * + * 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. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +/** + * RuntimeException descendant indicating a conformance problem during PDF generation. This + * exception will be throws, for example, when PDF/A-1 Level B conformance is activated but the + * PDF version produced is not PDF 1.4 as mandated by ISO 19005-1:2005(E). + */ +public class PDFConformanceException extends RuntimeException { + + /** + * Constructs an PDFConformanceException with no detail message. + * A detail message is a String that describes this particular exception. + */ + public PDFConformanceException() { + super(); + } + + /** + * Constructs an PDFConformanceException with the specified detail + * message. A detail message is a String that describes this particular + * exception. + * @param message the String that contains a detailed message + */ + public PDFConformanceException(String message) { + super(message); + } + +} diff --git a/src/java/org/apache/fop/pdf/PDFDocument.java b/src/java/org/apache/fop/pdf/PDFDocument.java index 572e3be43..916f19ca8 100644 --- a/src/java/org/apache/fop/pdf/PDFDocument.java +++ b/src/java/org/apache/fop/pdf/PDFDocument.java @@ -58,10 +58,6 @@ import org.apache.commons.logging.LogFactory; public class PDFDocument { private static final Integer LOCATION_PLACEHOLDER = new Integer(0); - /** - * the version of PDF supported which is 1.4 - */ - protected static final String PDF_VERSION = "1.4"; /** Integer constant to represent PDF 1.3 */ public static final int PDF_VERSION_1_3 = 3; @@ -107,6 +103,12 @@ public class PDFDocument { /** Indicates what PDF version is active */ protected int pdfVersion = PDF_VERSION_1_4; + /** + * Indicates the PDF/A-1 mode currently active. Defaults to "no restrictions", i.e. + * PDF/A-1 not active. + */ + protected PDFAMode pdfAMode = PDFAMode.DISABLED; + /** * the /Root object */ @@ -250,6 +252,35 @@ public class PDFDocument { return this.pdfVersion; } + /** @return the String representing the active PDF version */ + public String getPDFVersionString() { + switch (getPDFVersion()) { + case PDF_VERSION_1_3: + return "1.3"; + case PDF_VERSION_1_4: + return "1.4"; + default: + throw new IllegalStateException("Unsupported PDF version selected"); + } + } + + /** @return the PDF/A mode currently active. */ + public PDFAMode getPDFAMode() { + return this.pdfAMode; + } + + /** + * Sets the active PDF/A mode. This must be set immediately after calling the constructor so + * the checks will be activated. + * @param mode one of the PDFAMode constants + */ + public void setPDFAMode(PDFAMode mode) { + if (mode == null) { + throw new NullPointerException("mode must not be null"); + } + this.pdfAMode = mode; + } + /** * Returns the factory for PDF objects. * @return PDFFactory the factory @@ -474,8 +505,10 @@ public class PDFDocument { * @param params The encryption parameters for the pdf file */ public void setEncryption(PDFEncryptionParams params) { - this.encryption = - PDFEncryptionManager.newInstance(++this.objectcount, params); + if (getPDFAMode().isPDFA1LevelB()) { + throw new PDFConformanceException("PDF/A-1 doesn't allow encrypted PDFs"); + } + this.encryption = PDFEncryptionManager.newInstance(++this.objectcount, params); ((PDFObject)this.encryption).setDocument(this); if (encryption != null) { /**@todo this cast is ugly. PDFObject should be transformed to an interface. */ @@ -824,13 +857,16 @@ public class PDFDocument { public void outputHeader(OutputStream stream) throws IOException { this.position = 0; - byte[] pdf = ("%PDF-" + PDF_VERSION + "\n").getBytes(); + if (getPDFAMode().isPDFA1LevelB() && getPDFVersion() != PDF_VERSION_1_4) { + throw new PDFConformanceException("PDF version must be 1.4 for " + getPDFAMode()); + } + + byte[] pdf = ("%PDF-" + getPDFVersionString() + "\n").getBytes(); stream.write(pdf); this.position += pdf.length; // output a binary comment as recommended by the PDF spec (3.4.1) - byte[] bin = - { + byte[] bin = { (byte)'%', (byte)0xAA, (byte)0xAB, diff --git a/src/java/org/apache/fop/pdf/PDFInfo.java b/src/java/org/apache/fop/pdf/PDFInfo.java index 7bf346753..109d791bd 100644 --- a/src/java/org/apache/fop/pdf/PDFInfo.java +++ b/src/java/org/apache/fop/pdf/PDFInfo.java @@ -44,6 +44,11 @@ public class PDFInfo extends PDFObject { */ private String creator; + /** @return the producer of the document or null if not set */ + public String getProducer() { + return this.producer; + } + /** * set the producer string * @@ -110,7 +115,7 @@ public class PDFInfo extends PDFObject { } /** @return the keywords for the document or null if not set */ - public String getKeyword() { + public String getKeywords() { return this.keywords; } diff --git a/src/java/org/apache/fop/pdf/PDFMetadata.java b/src/java/org/apache/fop/pdf/PDFMetadata.java index d162ba032..9af42e8b6 100644 --- a/src/java/org/apache/fop/pdf/PDFMetadata.java +++ b/src/java/org/apache/fop/pdf/PDFMetadata.java @@ -23,6 +23,7 @@ import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.TimeZone; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; @@ -32,7 +33,6 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import org.apache.fop.apps.FOUserAgent; import org.apache.fop.fo.ElementMapping; import org.apache.fop.fo.extensions.xmp.XMPConstants; import org.w3c.dom.DOMImplementation; @@ -127,6 +127,11 @@ public class PDFMetadata extends PDFStream { /** @see org.apache.fop.pdf.AbstractPDFStream#buildStreamDict(String) */ protected String buildStreamDict(String lengthEntry) { final String filterEntry = getFilterList().buildFilterDictEntries(); + if (getDocumentSafely().getPDFAMode().isPDFA1LevelB() + && filterEntry != null && filterEntry.length() > 0) { + throw new PDFConformanceException( + "The Filter key is prohibited when PDF/A-1 is active"); + } final StringBuffer sb = new StringBuffer(128); sb.append(getObjectID()); sb.append("<< "); @@ -153,7 +158,8 @@ public class PDFMetadata extends PDFStream { Element desc, el; PDFInfo info = pdfDoc.getInfo(); DateFormat pseudoISO8601DateFormat = new SimpleDateFormat( - "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS"); + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"); + pseudoISO8601DateFormat.setTimeZone(TimeZone.getTimeZone("GMT+00")); //Set creation date if not available, yet if (info.getCreationDate() == null) { @@ -161,9 +167,12 @@ public class PDFMetadata extends PDFStream { info.setCreationDate(d); } + final String xmlns = "http://www.w3.org/2000/xmlns/"; + //Dublin Core desc = doc.createElementNS(XMPConstants.RDF_NAMESPACE, "rdf:Description"); desc.setAttribute("about", ""); + desc.setAttributeNS(xmlns, "xmlns:dc", XMPConstants.DUBLIN_CORE_NAMESPACE); rdf.appendChild(desc); if (info.getAuthor() != null) { el = doc.createElementNS(XMPConstants.DUBLIN_CORE_NAMESPACE, "dc:creator"); @@ -187,6 +196,7 @@ public class PDFMetadata extends PDFStream { //XMP Basic Schema desc = doc.createElementNS(XMPConstants.RDF_NAMESPACE, "rdf:Description"); desc.setAttribute("about", ""); + desc.setAttributeNS(xmlns, "xmlns:xmp", XMPConstants.XMP_BASIC_NAMESPACE); rdf.appendChild(desc); el = doc.createElementNS(XMPConstants.XMP_BASIC_NAMESPACE, "xmp:createDate"); desc.appendChild(el); @@ -197,6 +207,24 @@ public class PDFMetadata extends PDFStream { el.appendChild(doc.createTextNode(info.getCreator())); } + //Adobe PDF Schema + desc = doc.createElementNS(XMPConstants.RDF_NAMESPACE, "rdf:Description"); + desc.setAttribute("about", ""); + desc.setAttributeNS(xmlns, "xmlns:pdf", XMPConstants.ADOBE_PDF_NAMESPACE); + rdf.appendChild(desc); + if (info.getKeywords() != null) { + el = doc.createElementNS(XMPConstants.ADOBE_PDF_NAMESPACE, "pdf:Keywords"); + desc.appendChild(el); + el.appendChild(doc.createTextNode(info.getKeywords())); + } + if (info.getProducer() != null) { + el = doc.createElementNS(XMPConstants.ADOBE_PDF_NAMESPACE, "pdf:Producer"); + desc.appendChild(el); + el.appendChild(doc.createTextNode(info.getProducer())); + } + el = doc.createElementNS(XMPConstants.ADOBE_PDF_NAMESPACE, "pdf:PDFVersion"); + desc.appendChild(el); + el.appendChild(doc.createTextNode(pdfDoc.getPDFVersionString())); return doc; } diff --git a/src/java/org/apache/fop/render/pdf/PDFRenderer.java b/src/java/org/apache/fop/render/pdf/PDFRenderer.java index d0f65d16f..33602792d 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRenderer.java +++ b/src/java/org/apache/fop/render/pdf/PDFRenderer.java @@ -62,6 +62,7 @@ import org.apache.fop.fonts.FontSetup; import org.apache.fop.image.FopImage; import org.apache.fop.image.ImageFactory; import org.apache.fop.image.XMLImage; +import org.apache.fop.pdf.PDFAMode; import org.apache.fop.pdf.PDFAnnotList; import org.apache.fop.pdf.PDFColor; import org.apache.fop.pdf.PDFDocument; @@ -109,6 +110,9 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { */ public static final String MIME_TYPE = MimeConstants.MIME_PDF; + /** Rendering Options key for the PDF/A mode. */ + public static final String PDF_A_MODE = "pdf-a-mode"; + /** Controls whether comments are written to the PDF stream. */ protected static final boolean WRITE_COMMENTS = true; @@ -117,6 +121,9 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { */ protected PDFDocument pdfDoc; + /** the PDF/A mode (Default: disabled) */ + protected PDFAMode pdfAMode = PDFAMode.DISABLED; + /** * Map of pages using the PageViewport as the key * this is used for prepared pages that cannot be immediately @@ -230,6 +237,11 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } else { this.fontList.addAll(cfgFonts); } + + String s = cfg.getChild(PDF_A_MODE, true).getValue(null); + if (s != null) { + this.pdfAMode = PDFAMode.valueOf(s); + } } /** @@ -237,6 +249,10 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { */ public void setUserAgent(FOUserAgent agent) { super.setUserAgent(agent); + String s = (String)agent.getRendererOptions().get(PDF_A_MODE); + if (s != null) { + this.pdfAMode = PDFAMode.valueOf(s); + } } /** @@ -249,6 +265,7 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { ostream = stream; this.pdfDoc = new PDFDocument( userAgent.getProducer() != null ? userAgent.getProducer() : ""); + this.pdfDoc.setPDFAMode(this.pdfAMode); this.pdfDoc.setCreator(userAgent.getCreator()); this.pdfDoc.setCreationDate(userAgent.getCreationDate()); this.pdfDoc.getInfo().setAuthor(userAgent.getAuthor()); -- 2.39.5