/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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; import java.io.IOException; import java.io.OutputStream; import java.util.Date; import java.util.UUID; import javax.xml.transform.TransformerConfigurationException; import org.xml.sax.SAXException; import org.apache.xmlgraphics.xmp.Metadata; import org.apache.xmlgraphics.xmp.XMPSerializer; import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter; import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter; import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema; import org.apache.xmlgraphics.xmp.schemas.pdf.AdobePDFAdapter; import org.apache.xmlgraphics.xmp.schemas.pdf.AdobePDFSchema; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFAAdapter; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFAXMPSchema; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFUAAdapter; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFUAXMPSchema; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFVTAdapter; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFVTXMPSchema; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFXAdapter; import org.apache.xmlgraphics.xmp.schemas.pdf.PDFXXMPSchema; import org.apache.xmlgraphics.xmp.schemas.pdf.XAPMMAdapter; import org.apache.xmlgraphics.xmp.schemas.pdf.XAPMMXMPSchema; /** * Special PDFStream for Metadata. * @since PDF 1.4 */ public class PDFMetadata extends PDFStream { private Metadata xmpMetadata; private boolean readOnly = true; /** * @param xmp xmp metadata * @param readOnly true if read only * @see org.apache.fop.pdf.PDFObject#PDFObject() */ public PDFMetadata(Metadata xmp, boolean readOnly) { super(); if (xmp == null) { throw new NullPointerException( "The parameter for the XMP Document must not be null"); } this.xmpMetadata = xmp; this.readOnly = readOnly; } /** {@inheritDoc} */ protected String getDefaultFilterName() { return PDFFilterList.METADATA_FILTER; } /** * @return the XMP metadata */ public Metadata getMetadata() { return this.xmpMetadata; } /** * overload the base object method so we don't have to copy * byte arrays around so much * {@inheritDoc} */ public int output(java.io.OutputStream stream) throws java.io.IOException { int length = super.output(stream); this.xmpMetadata = null; //Release DOM when it's not used anymore return length; } /** {@inheritDoc} */ protected void outputRawStreamData(OutputStream out) throws IOException { try { XMPSerializer.writeXMPPacket(xmpMetadata, out, this.readOnly); } catch (TransformerConfigurationException tce) { throw new IOException("Error setting up Transformer for XMP stream serialization: " + tce.getMessage()); } catch (SAXException saxe) { throw new IOException("Error while serializing XMP stream: " + saxe.getMessage()); } } /** {@inheritDoc} */ protected void populateStreamDict(Object lengthEntry) { final String filterEntry = getFilterList().buildFilterDictEntries(); if (getDocumentSafely().getProfile().getPDFAMode().isPart1() && filterEntry != null && filterEntry.length() > 0) { throw new PDFConformanceException( "The Filter key is prohibited when PDF/A-1 is active"); } put("Type", new PDFName("Metadata")); put("Subtype", new PDFName("XML")); super.populateStreamDict(lengthEntry); } /** * Creates an XMP document based on the settings on the PDF Document. * @param pdfDoc the PDF Document * @return the requested XMP metadata */ public static Metadata createXMPFromPDFDocument(PDFDocument pdfDoc) { Metadata meta = new Metadata(); PDFInfo info = pdfDoc.getInfo(); PDFRoot root = pdfDoc.getRoot(); //Set creation date if not available, yet if (info.getCreationDate() == null) { Date d = new Date(); info.setCreationDate(d); } //Important: Acrobat 7's preflight check for PDF/A-1b wants the creation date in the Info //object and in the XMP metadata to have the same timezone or else it shows a validation //error even if the times are essentially equal. //Dublin Core DublinCoreAdapter dc = DublinCoreSchema.getAdapter(meta); //PDF/A identification PDFAMode pdfaMode = pdfDoc.getProfile().getPDFAMode(); dc.setCompact(pdfaMode.getPart() != 3); if (info.getAuthor() != null) { dc.addCreator(info.getAuthor()); } if (info.getTitle() != null) { dc.setTitle(info.getTitle()); } if (info.getSubject() != null) { //Subject maps to dc:description["x-default"] as per ISO-19005-1:2005/Cor.1:2007 dc.setDescription(null, info.getSubject()); } if (root.getLanguage() != null) { //Note: No check is performed to make sure the value is valid RFC 3066! dc.addLanguage(root.getLanguage()); } dc.addDate(info.getCreationDate()); //Somewhat redundant but some PDF/A checkers issue a warning without this. dc.setFormat("application/pdf"); PDFUAMode pdfuaMode = pdfDoc.getProfile().getPDFUAMode(); if (pdfuaMode.isEnabled()) { PDFUAAdapter pdfua = PDFUAXMPSchema.getAdapter(meta); pdfua.setPart(pdfuaMode.getPart()); } if (pdfaMode.isEnabled()) { PDFAAdapter pdfa = PDFAXMPSchema.getAdapter(meta); pdfa.setPart(pdfaMode.getPart()); pdfa.setConformance(String.valueOf(pdfaMode.getConformanceLevel())); } AdobePDFAdapter adobePDF = AdobePDFSchema.getAdapter(meta); PDFXMode pdfxMode = pdfDoc.getProfile().getPDFXMode(); if (pdfxMode != PDFXMode.DISABLED) { PDFXAdapter pdfx = PDFXXMPSchema.getAdapter(meta); pdfx.setVersion(pdfxMode.getName()); XAPMMAdapter xapmm = XAPMMXMPSchema.getAdapter(meta); xapmm.setVersion("1"); xapmm.setDocumentID("uuid:" + UUID.randomUUID().toString()); xapmm.setInstanceID("uuid:" + UUID.randomUUID().toString()); xapmm.setRenditionClass("default"); adobePDF.setTrapped("False"); } PDFProfile profile = pdfDoc.getProfile(); PDFVTMode pdfvtMode = profile.getPDFVTMode(); if (pdfvtMode != PDFVTMode.DISABLED) { PDFVTAdapter pdfvt = PDFVTXMPSchema.getAdapter(meta); pdfvt.setVersion("PDF/VT-1"); if (info.getModDate() != null) { pdfvt.setModifyDate(info.getModDate()); } else if (profile.isModDateRequired()) { //if modify date is needed but none is in the Info object, use creation date pdfvt.setModifyDate(info.getCreationDate()); } } //XMP Basic Schema XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(meta); xmpBasic.setCreateDate(info.getCreationDate()); if (info.getModDate() != null) { xmpBasic.setModifyDate(info.getModDate()); } else if (profile.isModDateRequired()) { //if modify date is needed but none is in the Info object, use creation date xmpBasic.setModifyDate(info.getCreationDate()); } if (info.getCreator() != null) { xmpBasic.setCreatorTool(info.getCreator()); } if (info.getKeywords() != null) { adobePDF.setKeywords(info.getKeywords()); } if (info.getProducer() != null) { adobePDF.setProducer(info.getProducer()); } adobePDF.setPDFVersion(pdfDoc.getPDFVersionString()); return meta; } /** * Updates the values in the Info object from the XMP metadata according to the rules defined * in PDF/A-1 (ISO 19005-1:2005) * @param meta the metadata * @param info the Info object */ public static void updateInfoFromMetadata(Metadata meta, PDFInfo info) { DublinCoreAdapter dc = DublinCoreSchema.getAdapter(meta); info.setTitle(dc.getTitle()); String[] creators = dc.getCreators(); if (creators != null && creators.length > 0) { info.setAuthor(creators[0]); } else { info.setAuthor(null); } //dc:description["x-default"] maps to Subject as per ISO-19005-1:2005/Cor.1:2007 info.setSubject(dc.getDescription()); AdobePDFAdapter pdf = AdobePDFSchema.getAdapter(meta); info.setKeywords(pdf.getKeywords()); info.setProducer(pdf.getProducer()); XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(meta); info.setCreator(xmpBasic.getCreatorTool()); Date d; d = xmpBasic.getCreateDate(); xmpBasic.setCreateDate(d); //To make Adobe Acrobat happy (bug filed with Adobe) //Adobe Acrobat doesn't like it when the xmp:CreateDate has a different timezone //than Info/CreationDate info.setCreationDate(d); d = xmpBasic.getModifyDate(); if (d != null) { //ModifyDate is only required for PDF/X xmpBasic.setModifyDate(d); info.setModDate(d); } } }