All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.xfa.xmp.XMPHelper Maven / Gradle / Ivy

The newest version!
/*
 * ADOBE CONFIDENTIAL
 *
 * Copyright 2008 Adobe Systems Incorporated All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property of
 * Adobe Systems Incorporated and its suppliers, if any. The intellectual and
 * technical concepts contained herein are proprietary to Adobe Systems
 * Incorporated and its suppliers and may be covered by U.S. and Foreign
 * Patents, patents in process, and are protected by trade secret or copyright
 * law. Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained from
 * Adobe Systems Incorporated.
 */
package com.adobe.xfa.xmp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import org.apache.xerces.util.XMLChar;

import com.adobe.internal.xmp.XMPConst;
import com.adobe.internal.xmp.XMPDateTime;
import com.adobe.internal.xmp.XMPDateTimeFactory;
import com.adobe.internal.xmp.XMPException;
import com.adobe.internal.xmp.XMPMeta;
import com.adobe.internal.xmp.XMPMetaFactory;
import com.adobe.internal.xmp.XMPPathFactory;
import com.adobe.internal.xmp.XMPUtils;
import com.adobe.internal.xmp.options.PropertyOptions;
import com.adobe.internal.xmp.options.SerializeOptions;
import com.adobe.internal.xmp.properties.XMPProperty;

import com.adobe.xfa.AppModel;
import com.adobe.xfa.Attribute;
import com.adobe.xfa.DOMSaveOptions;
import com.adobe.xfa.Element;
import com.adobe.xfa.Model;
import com.adobe.xfa.Node;
import com.adobe.xfa.RichTextNode;
import com.adobe.xfa.StringAttr;
import com.adobe.xfa.TextNode;
import com.adobe.xfa.XFA;
import com.adobe.xfa.XMLMultiSelectNode;
import com.adobe.xfa.content.BooleanValue;
import com.adobe.xfa.content.DateTimeValue;
import com.adobe.xfa.content.DateValue;
import com.adobe.xfa.content.DecimalValue;
import com.adobe.xfa.content.ExDataValue;
import com.adobe.xfa.content.FloatValue;
import com.adobe.xfa.content.ImageValue;
import com.adobe.xfa.content.IntegerValue;
import com.adobe.xfa.content.TextValue;
import com.adobe.xfa.content.TimeValue;
import com.adobe.xfa.template.TemplateModel;
import com.adobe.xfa.ut.Base64;
import com.adobe.xfa.ut.ExFull;
import com.adobe.xfa.ut.ResId;
import com.adobe.xfa.ut.StringUtils;
import com.adobe.xfa.ut.UniCharIterator;


/**
 * @exclude from published api.
 */
public class XMPHelper {
	
	private final static String XMP_NS_DESC_URI = "http://ns.adobe.com/xfa/promoted-desc/";
	private final static String XMP_NS_DESC_PREFIX = "desc";
	
	public final static int OUTPUTTYPE_RDF = 0;
	// PlainXMP support is being removed from Java and C++
	//private final static int OUTPUTTYPE_PLAINXMP = 1;
	
	private final String 	msDateTime;
	private final XMPMeta 	mXMP;
	private final boolean 	mbAllowTemplateUpdates;
	// use a pointer instead of a wrapper otherwise we will get memory leaks
	private final AppModel	mAppModel;
	
	private String		msCreatorToolValue;
	private String		msProducerValue;

	// PDF/A metadata, as defined in the xci.
	// See http://xtg.can.adobe.com/twiki/bin/view/XFA/PDFAConfigXFAProposal.
	// version
	private int			miPart;
	// version 1 has conformance levels ("A" and "B")
	private String		msConformance;
	// amendments to the above (e.g. "", "2006:1")
	private String		msAmendment;
	

	public static byte[] getXMPPacket(AppModel appModel) {
		// grab the xmpmeta packet and convert it to a byte[].
		final String sXMPmeta = "xmpmeta";
		final Element xmpPacket = (Element)appModel.resolveNode(sXMPmeta);
		if (xmpPacket instanceof Element) {
			final ByteArrayOutputStream originalXMP = new ByteArrayOutputStream();
			final DOMSaveOptions saveOpts = new DOMSaveOptions();
			saveOpts.setDisplayFormat (DOMSaveOptions.RAW_OUTPUT);
			saveOpts.setIndentLevel(0);
			saveOpts.setExcludePreamble(true);
			xmpPacket.saveXML(originalXMP, saveOpts);
			return originalXMP.toByteArray();
		}
		
		return null;
	}

	public XMPHelper(AppModel appModel,
			  byte[] xmp /* = null */,
			  String sDateTime /* = "" */,
			  boolean bAllowTemplateUpdates /* = true */ /* should be FALSE when invoking $form.metadata from script */ ) throws XMPException {
		
		mAppModel = appModel;
		
		if (xmp != null)
			mXMP = XMPMetaFactory.parseFromBuffer(xmp);
		else
			mXMP = XMPMetaFactory.create();
			
		msDateTime = sDateTime;
		mbAllowTemplateUpdates = bAllowTemplateUpdates;
		miPart = 0;	
		
		// don't grab the template model and store it here 
		// because it may not be available at the time of construction
		
		// In the C++, this happens in XMPWrapper::init()
		XMPMetaFactory.getSchemaRegistry().registerNamespace(XMP_NS_DESC_URI, XMP_NS_DESC_PREFIX);
	}
	
	private void createTextNode (
					String sXMPArrayName,
					int nIndex,
					TemplateModel templateModel,
					Element parentNode,
					Element descNode,
					String sXFAName) throws XMPException {
		
		final XMPProperty value = mXMP.getArrayItem(XMPConst.NS_DC, sXMPArrayName, nIndex);
		if (value != null) {
			// find the node
			Element oNode = (Element)descNode.resolveNode("$." + sXFAName);
			if (oNode == null) {
				oNode = new TextValue(descNode, null);
				oNode.setName(sXFAName);
			}
			
			final TextNode textNode = oNode.getText(false, false, false);
			textNode.setValue(value.toString(), true, false);
		}
	}
	
	// is the XMP metadata more current than the top level subform?
	private boolean	useXMP() throws XMPException {
		final TemplateModel templateModel = TemplateModel.getTemplateModel(mAppModel, false);
		if (templateModel == null)
			return true;
		
		final Node desc = templateModel.resolveNode("$template.#subform.#desc");
		boolean bRetValue = true;
		// if there is no desc element, we put XMP metadata into the top level subform
		if (desc != null) {
			final String gsTimeStamp = "MetadataDate";
			final XMPDateTime xmpTimeStamp = mXMP.getPropertyDate(XMPConst.NS_XMP, gsTimeStamp);
			String sMetaDate = null;
			Attribute attr = mAppModel.getAttribute(XFA.TIMESTAMPTAG);
			if (attr != null) {
				sMetaDate = attr.toString();
			}
				
			// if there is no timeStamp attribute and the top level subform has
			// no desc element, use the XMP metadata
			if (xmpTimeStamp == null) {
				bRetValue = false;
			}
			else if (StringUtils.isEmpty(sMetaDate)) {
				bRetValue = true;
			}
			else {
				final XMPDateTime xfaTimeStamp = XMPUtils.convertToDate(sMetaDate);
				bRetValue = xmpTimeStamp.getCalendar().after(xfaTimeStamp);
			}
		}
		
		return bRetValue;
	}


// Script property support
	public String metadata (int nOutputType) throws XMPException {
		
		// PlainXMP support is being removed from Java and C++
		if (nOutputType != OUTPUTTYPE_RDF)
			throw new ExFull(ResId.UNSUPPORTED_OPERATION, "XMPHelper#metadata - PlainXMP");
		
		synchronize(false);
		processOtherDesc ();
		
		int optionsFlags = SerializeOptions.ENCODE_UTF8 | SerializeOptions.OMIT_PACKET_WRAPPER;
		SerializeOptions options = new SerializeOptions(optionsFlags);
		options.setIndent("   ");
		
		return XMPMetaFactory.serializeToString(mXMP, options);
	}

	public void	processTemplateDesc () throws XMPException {
		// push all of the top level subform desc values
		// into the XMP
		final TemplateModel templateModel = TemplateModel.getTemplateModel(mAppModel, false);
		if (templateModel == null)
			return;

		{
			// go and grab the passed in timestamp as the current appModel doesn't have the current one
			final XMPDateTime sXMPTimeStamp = StringUtils.isEmpty(msDateTime) ? XMPDateTimeFactory.getCurrentDateTime() : XMPDateTimeFactory.createFromISO8601(msDateTime);
			final String gsTimeStamp = "MetadataDate";
			mXMP.setPropertyDate(XMPConst.NS_XMP, gsTimeStamp, sXMPTimeStamp);
		}

		// handle the creatortool
		if (msCreatorToolValue != null) {
			final String gsCreatorTool = "CreatorTool";
			mXMP.setProperty(XMPConst.NS_XMP, gsCreatorTool, msCreatorToolValue);
		}

		// add the PDF/A version
		if ( miPart != 0 ) {
			processPDFAVersionInfo();
		}
		
		// handle the producer
		if (msProducerValue != null) {
			final String gsProducer = "Producer";
			mXMP.setProperty(XMPConst.NS_PDF, gsProducer, msProducerValue);
		}

		// handle the documentid
		{
			final Attribute oUUId = mAppModel.getAttribute(XFA.UUIDTAG);

			if (oUUId != null) {
				final String sUUId = oUUId.toString();
				if (!StringUtils.isEmpty(sUUId)) {
					final String gsDocumentID = "DocumentID";
					final String gsUuid = "uuid:";
					final String sDocumentIdValue = gsUuid + sUUId;
					mXMP.setProperty(XMPConst.NS_XMP_MM, gsDocumentID, sDocumentIdValue);
				}
			}
		}


		// Clean out the desc: properties and the dc: properties we will generate
		XMPUtils.removeProperties(mXMP, XMP_NS_DESC_URI, null, true, true);

		final String gsDate = "date";
		final String gsDescription = "description";
		final String gsCreator = "creator";
		final String gsTitle = "title";

		mXMP.deleteProperty(XMPConst.NS_DC, gsDate);
		mXMP.deleteProperty(XMPConst.NS_DC, gsDescription);
		mXMP.deleteProperty(XMPConst.NS_DC, gsCreator);
		mXMP.deleteProperty(XMPConst.NS_DC, gsTitle);			

		final Node desc = templateModel.resolveNode("$template.#subform.#desc");
		if (desc != null) {
			// set DublinCore metatdata in the dc schema

			final String gsElementRefinement = "element-refinement";
			final String gsDCPrefix = "dc:";

			// Watson 1923416: in PDF/A include the dc: elements that correspond to the /Info dictionary.
			// miPart is 0 for non-PDFA documents.
			if (miPart == 0) {
				// handle the created date
				String sCreated = "";
				final String gsCreated = "created";
				{
					final Node oNode = desc.resolveNode ("$." + gsCreated);
					if (oNode instanceof TextValue) {
						sCreated = ((TextValue)oNode).getValue();
					}
				}

				// handle the issued date
				String sIssued = "";
				final String gsIssued = "issued";
				{
					final Node node = desc.resolveNode("$." + gsIssued);
					if (node instanceof TextValue) {
						sIssued = ((TextValue)node).getValue();
					}
				}

				// create date property
				if (!StringUtils.isEmpty(sCreated) || !StringUtils.isEmpty(sIssued)) {
					mXMP.setProperty(XMPConst.NS_DC, gsDate, null, new PropertyOptions(PropertyOptions.ARRAY_ORDERED));
				}

				int nIndex = 1;
				if (!StringUtils.isEmpty(sCreated)) {
					// store created date
					mXMP.appendArrayItem(XMPConst.NS_DC, gsDate, sCreated);
					final String sPath = gsDate + "[1]";
					final String sQualifierName = gsDCPrefix + gsCreated;
					mXMP.setQualifier(XMPConst.NS_DC, sPath, XMPConst.NS_DC, gsElementRefinement, sQualifierName);
					nIndex++;
				}					
				if (!StringUtils.isEmpty(sIssued)) {
					// store issued date
					mXMP.appendArrayItem (XMPConst.NS_DC, gsDate, sIssued);
					final String sPath = XMPPathFactory.composeArrayItemPath(gsDate, nIndex);
					final String sQualifierName = gsDCPrefix + gsIssued;
					mXMP.setQualifier(XMPConst.NS_DC, sPath, XMPConst.NS_DC, gsElementRefinement, sQualifierName);
				}
			}
			{
				// handle the description
				String sDescription = "";
				{
					final Node node = desc.resolveNode ("$." + gsDescription);
					if (node instanceof TextValue) {
						sDescription = ((TextValue)node).getValue();
					}
				}
				if (!StringUtils.isEmpty(sDescription)) {
					// might be more appropriate to make this more robust
					// by allowing additional descriptions sensitive to language
					// instead of xml:lang="x-default", maybe use the locale of the template
					mXMP.setLocalizedText(XMPConst.NS_DC, gsDescription, null, "x-default", sDescription);
				}
			}

			// handle the creator (that is the author, as opposed to creatortool)
			String sCreator = null;
			{
				final Node node = desc.resolveNode ("$." + gsCreator);
				if (node instanceof TextValue) {
					sCreator = ((TextValue)node).getValue();
				}
			}

			if (!StringUtils.isEmpty(sCreator)) {
				// create creator property
				mXMP.setProperty(XMPConst.NS_DC, gsCreator, null, new PropertyOptions(PropertyOptions.ARRAY_ORDERED));
				mXMP.appendArrayItem(XMPConst.NS_DC, gsCreator, sCreator);
			}
				
			// handle the title
			String sTitle = null;
			{
				final Node node = desc.resolveNode ("$." + gsTitle);
				if (node instanceof TextValue) {
					sTitle = ((TextValue)node).getValue();
				}
			}

			if (!StringUtils.isEmpty(sTitle)) {
				// might be more appropriate to make this more robust
				// by allowing additional titles sensitive to language
				// instead of xml:lang="x-default", maybe use the locale of the template
				mXMP.setLocalizedText(XMPConst.NS_DC, gsTitle, null, "x-default", sTitle);
			}

			// Process any custom metadata.
			processDesc(desc.getXFAParent());
		}
	}
	
	public void	processOtherDesc () throws XMPException {
		final TemplateModel templateModel = TemplateModel.getTemplateModel(mAppModel, false);
		if (templateModel == null)
			return;
		
		final Node topSubForm = templateModel.resolveNode("$template.#subform");
		if (topSubForm == null)
			return;
		
		for (Node child = topSubForm.getFirstXFAChild(); child != null; child = child.getNextXFASibling()) {
			// skip the desc element on the top level subform
			// as it is handled in processTemplateDesc
			if (child.getClassTag() != XFA.DESCTAG) {
				metadata_helper(child);
			}
		}
	}
	
	public void	processXMP () throws XMPException {
		// extract from the XMP all of the values which correspond to the
		// top level subform desc values
		final TemplateModel templateModel = TemplateModel.getTemplateModel(mAppModel, false);
		if (templateModel == null)
			return;

		// handle timeStamp
		{
			final String gsMetadataDate = "MetadataDate";
			final String sXMPTimeStamp = mXMP.getPropertyString(XMPConst.NS_XMP, gsMetadataDate);
			if (sXMPTimeStamp != null) {
				// TODO: Need to do this bypassing schema checks
				mAppModel.setAttribute(new StringAttr(XFA.TIMESTAMP, sXMPTimeStamp), XFA.TIMESTAMPTAG);
			}
		}
		// handle uuid
		{
			final String gsDocumentID = "DocumentID";
			final String sUUId = "uuid:";
			String sDocumentIdValue = mXMP.getPropertyString(XMPConst.NS_XMP_MM, gsDocumentID);
			if (sDocumentIdValue != null) {
				// swallow "uuid:" in string
				assert sDocumentIdValue.startsWith(sUUId);
				sDocumentIdValue = sDocumentIdValue.substring(sUUId.length());
				// TODO: Need to do this bypassing schema checks
				mAppModel.setAttribute(new StringAttr(XFA.UUID, sDocumentIdValue), XFA.UUIDTAG);
			}
		}

		final Element topSubForm = (Element)templateModel.resolveNode("$template.#subform");
		if (topSubForm == null)
			return;
		
		final Element desc = (Element)topSubForm.resolveNode("$.#desc");
		if (desc == null)
			return;

		final String gsDate = "date";
		final String gsCreated = "created";
		final String gsIssued = "issued";
		final String gsDescription = "description";
		final String gsCreator = "creator";
		final String gsTitle = "title";

		// handle the created date
		createTextNode(gsDate, 1, templateModel, topSubForm, desc, gsCreated);

		// handle the issued date
		createTextNode(gsDate, 2, templateModel, topSubForm, desc, gsIssued);

		// handle the description
		createTextNode(gsDescription, 1, templateModel, topSubForm, desc, gsDescription);

		// handle the creator (that is the author, as opposed to creatortool)
		createTextNode(gsCreator, 1, templateModel, topSubForm, desc, gsCreator);

		// handle the title
		createTextNode(gsTitle, 1, templateModel, topSubForm, desc, gsTitle);

		// TBD: handle version, contact, department, etcetera
	}
	
	public void	processDesc(Element element) throws XMPException {
		// miPart is 0 for non-PDFA documents.  metadata is excluded
		// from PDF/A-1 but could be reinstated in later versions of PDF/A.
		if ( miPart != 0 )
			return;
		
		final Element desc = element.peekElement(XFA.DESCTAG, false, 0);
		if (desc != null) {
			for (Node child = desc.getFirstXFAChild(); child != null; child = child.getNextXFASibling()) { 
				String aName = child.getName();
				// when the individual desc value doesn't have a name, use the field, draw, or subform name
				if (aName == "") {
					aName = element.getName();
				}
				// need to ensure name is a valid XML tag
				final String sName = fixupName(aName);

				// Filter out the special tags that are handled by processTemplateDesc.  It only
				// looks at the top-level subform, and this filter applies to all subforms, but XMP
				// properties are scopeless anyway.
				if (sName.equals("created") ||
					sName.equals("issued") ||
					sName.equals("description") ||
					sName.equals("creator") ||
					sName.equals("title"))
					continue;	// specially handled

				// this part is a bit dodgey - if the name already exists in the XMP metadata
				// any subsequent entries will not added
				// in future, it might make sense to delete the original property and
				// replace it with an array
				
				if (mXMP.getProperty(XMP_NS_DESC_URI, sName) == null) {
					
					// map the XFA value element to the XMP type
					boolean bProcessedChild = false;
					boolean bEmbeddedImage = false;
					
					if (child instanceof TextValue) {
						final String sValue = ((TextValue)child).getValue();
						mXMP.setProperty(XMP_NS_DESC_URI, sName, sValue);
						bProcessedChild = true;
					}
					else if (child instanceof ExDataValue) {
						final ExDataValue exDataValue = (ExDataValue)child;
						
						if (exDataValue.isPropertySpecified(XFA.HREFTAG, true, 0)) {
							final Attribute oHref = exDataValue.getAttribute(XFA.HREFTAG);
							final String sHref = oHref.toString();
							mXMP.setProperty(XMP_NS_DESC_URI, sName, sHref, new PropertyOptions(PropertyOptions.URI));
							bProcessedChild = true;
						}
						else {
							// Encode contents of exData into the URI, as per RFC2397 ().
							final Node node = exDataValue.getFirstXFAChild();	// TBD is first/only child sufficient?
							
							byte[] value = null;
							
							if (node instanceof TextNode) {
								// JavaPort: This matches the behaviour of the C++ implementation
								// that encodes all contents of exData, but we could actually
								// deal with text as plain text.
								
								try {
									value = ((TextNode)node).getValue().getBytes("UTF-8");
								}
								catch (UnsupportedEncodingException ex) {
									// not possible
								}
							}
							else if (node instanceof XMLMultiSelectNode || node instanceof RichTextNode) {
							
								value = getXML((Element)node);
							}
							
							final String sBase64Encoded = Base64.encode(value, true);	
							final String sHref = "data:;base64," + sBase64Encoded;
							mXMP.setProperty(XMP_NS_DESC_URI, sName, sHref, new PropertyOptions(PropertyOptions.URI));

							// add the contentType expression as a qualifier on the URI
							final String gsContentType = "contentType";
							final Attribute oContentType = ((Element)child).getAttribute(XFA.CONTENTTYPETAG);
							final String sContentTypeValue = oContentType.toString();
							mXMP.setQualifier(XMP_NS_DESC_URI, sName, XMP_NS_DESC_URI, gsContentType, sContentTypeValue);
							bProcessedChild = true;
						}
					}
					else if (child instanceof ImageValue) {
						final ImageValue imageValue = (ImageValue)child;
						// Note that getValue for an XFAImage returns the contents of the href tag if it's specified,
						// and otherwise returns the inline data.
						final String sValue = imageValue.getValue();

						if (child.isPropertySpecified(XFA.HREFTAG, true, 0))
							mXMP.setProperty (XMP_NS_DESC_URI, sName, sValue, new PropertyOptions(PropertyOptions.URI));
						else {
							final String sContentType = imageValue.getProperty(XFA.CONTENTTYPETAG, 0).toString();
							final String sTransferEncoding = imageValue.getProperty(XFA.TRANSFERENCODINGTAG, 0).toString();
							setImageProperty(XMP_NS_DESC_URI, sName, sContentType, sTransferEncoding, sValue);
							bEmbeddedImage = true;
						}

						bProcessedChild = true;
					}
					else if (child instanceof DecimalValue) {
						final double dValue = ((DecimalValue)child).getValue();
						mXMP.setPropertyDouble(XMP_NS_DESC_URI, sName, dValue);
						bProcessedChild = true;
					}
					else if (child instanceof FloatValue) {
						final double dValue = ((FloatValue)child).getValue();
						mXMP.setPropertyDouble(XMP_NS_DESC_URI, sName, dValue);
						bProcessedChild = true;
					}
					else if (child instanceof IntegerValue) {
						final int iValue = ((IntegerValue)child).getValue();
						mXMP.setPropertyInteger(XMP_NS_DESC_URI, sName, iValue);
						bProcessedChild = true;
					}
					else if (child instanceof DateValue) {
						final String sValue = ((DateValue)child).getValue();
						final XMPDateTime dateValue = XMPDateTimeFactory.createFromISO8601(sValue);
						mXMP.setPropertyDate(XMP_NS_DESC_URI, sName, dateValue);
						bProcessedChild = true;
					}
					else if (child instanceof TimeValue) {
						final String sValue = ((TimeValue)child).getValue();
						final XMPDateTime dateValue = XMPDateTimeFactory.createFromISO8601(sValue);
						mXMP.setPropertyDate(XMP_NS_DESC_URI, sName, dateValue);
						bProcessedChild = true;
					}
					else if (child instanceof DateTimeValue) {
						final String sValue = ((DateTimeValue)child).getValue();
						final XMPDateTime dateValue = XMPDateTimeFactory.createFromISO8601(sValue);
						mXMP.setPropertyDate(XMP_NS_DESC_URI, sName, dateValue);
						bProcessedChild = true;
					}
					else if (child instanceof BooleanValue) {
						final boolean bValue = ((BooleanValue)child).getValue();
						mXMP.setPropertyBoolean(XMP_NS_DESC_URI, sName, bValue);
						bProcessedChild = true;
					}

					// Embedded images show up in a separate section, so don't attempt to create a reference to them.
					if (bProcessedChild && ! bEmbeddedImage) {
						// add the XPATH expression as a qualifier on the name
						final String gsRef = "ref";
						final String sXPathValue = getXPATH(element);
						mXMP.setQualifier(XMP_NS_DESC_URI, sName, XMP_NS_DESC_URI, gsRef, sXPathValue);
					}
				}
			}
		}
	}
	
	public String getXPATH(Element oNode) {
		final Map nameSpacePrefixList = new HashMap();
		final Model model = oNode.getModel();
		final StringBuilder sXPATH = new StringBuilder(oNode.getXPath(nameSpacePrefixList, model));
		int nSlashPos = 0;
		int nColonPos = 0;
		int nLastPos = 0;
		// this is a hack - remove the namespace prefixes inserted by jfDomNode::getXPath
		// The template namespace is normalized to the current version
		// in PDFLDriverBase::embedXFA which happens after calling PDFLDriverBase::generateXMP.
		// At this point, we can't put in the current template namespace into oNameSpacePrefixList
		// as it may not match what is in the template, so just trim out any prefixes which are thrown at us
		// This will have to do for now
		while (nLastPos < sXPATH.length() && (nSlashPos = sXPATH.indexOf("/", nLastPos)) != -1) {
			if ((nColonPos = sXPATH.indexOf(":", nSlashPos)) != -1) {
				sXPATH.replace(nSlashPos, nColonPos - nSlashPos + 1, "/");
			}
			nLastPos = nSlashPos + 1;
		}
		
		return "/template" + sXPATH;
	}
	
	/**
	 * Change the name to be a valid XFA name.
	 * Replace all illegal XFA name characters with underscore and
	 * ensure name does not start with a underscore.
	 */
	public static String fixupName(String sName) {
		// this method is using the XFA-Names-Alang-Forms-Proposal
		// https://eroom.adobe.com/eRoom/fid001/ALang/0_8262, which is a variant
		// on http://www.w3.org/TR/2004/REC-xml-20040204/#NT-Names
		
		// An XFA name is defined by the following productions:
		// Name ::= Letter, (NameChar)*
		// NameChar ::= Letter | Digit | '-' | '_' | CombiningChar | Extender

		// FIXME: This is incorrect in the Java version because we don't have
		// a way of testing specifically for an XML Letter. This is approximated
		// by testing for an NCName character, and then eliminating the ones we can test for.
		
		final StringBuilder sRetValue = new StringBuilder(sName.length());
		final UniCharIterator it = new UniCharIterator(sName);
		
		boolean bFirstChar = true;
		while (!it.isAtEnd()) {
			final int ch = it.next();
			boolean valid = XMLChar.isNCName(ch);
			
			if (valid) {
			
				// code points > 0xFFFF are not valid name chars, so this cast is safe
				// as long if the preceding test succeeded.
				final char c = (char)ch;
				
				if (bFirstChar) {
					bFirstChar = false;
					
					// FIXME: This isn't quite correct since it would allow CombiningChar or Extender
					if (Character.isDigit(c) || c == '.' || c == '_' || c == '-')
						continue; // drop the initial invalid character
				}
				else {
					valid = c != '.';
				}
			}
			
			sRetValue.append(valid ? (char)ch : '_');
		}
		
		return sRetValue.toString();
	}

	// By default, don't update .  This is for backward compatibility,
	// but really it should always have been this way.
	public void	synchronize(boolean bUpdateXMPPacket /* = false */) throws XMPException {
		if (useXMP()) {
			
			// Part of the fix for Watson 1919534.
			// Always update the producer: Designer sets pdf:Producer to itself in the xmpmeta packet,
			// but it should be set to either the default value or the value in config.pdf.producer,
			// which gets routed to XMPHelper via PDFLDriverBase::generateXMP().
			if (msProducerValue != null) {
				final String gsProducer = "Producer";
				mXMP.setProperty(XMPConst.NS_PDF, gsProducer, msProducerValue);
			}

			// Watson 1935665 - for PDF/A, remove any custom elements, including dc:date, added by Designer
			// and add the PDF/A version.
			if (miPart != 0) {
				XMPUtils.removeProperties(mXMP, XMP_NS_DESC_URI, null, true, true);
				final String gsDate = "date";
				mXMP.deleteProperty(XMPConst.NS_DC, gsDate);
				processPDFAVersionInfo();
			}

			if (mbAllowTemplateUpdates) {
				// copy properties from XMP metadata into the top level subform
				// when not called from script, that is
				// only from PDFLDriverBase::generateXMP
				processXMP();
			}
		}
		else {
			
			// copy properties from the top level subform into the XMP metadata
			processTemplateDesc();
			
			if (bUpdateXMPPacket) {
				
				// Replace the contents for the  packet with what's
				// in moXMP now.
		
				SerializeOptions options = new SerializeOptions(SerializeOptions.ENCODE_UTF8 | SerializeOptions.OMIT_PACKET_WRAPPER);
				options.setIndent("   ");
				
				final byte[] buffer = XMPMetaFactory.serializeToBuffer(mXMP, options);
				saveXMPPacket(buffer);
			}
		}
	}

	/**
	 *  save new xmp packet 
	 *  note this will not create a new XMP packet - packet must exist first 
	 */
	private void saveXMPPacket(byte[] buffer) {
		final String sXMPmeta = "xmpmeta";
		final Element xmpPacket = (Element)mAppModel.resolveNode(sXMPmeta);
		if (xmpPacket != null) {					
			xmpPacket.loadXML(new ByteArrayInputStream(buffer), true, true);
					
			// Insert a newline as the first child of the xmpmeta element.
			// This isn't particularly a functional issue, but makes the pretty
			// formatting look right so that the unit tests pass.
			TextNode textNode = new TextNode(null, null, "\n   ");
			xmpPacket.insertChild(textNode, xmpPacket.getFirstXMLChild(), false);
		}
	}
	
	// Set CreatorTool
	public void	setCreatorTool(String sCreatorTool, boolean bForceApply /*=false */) throws XMPException {
		msCreatorToolValue = sCreatorTool;
		if (bForceApply) {
			if (msCreatorToolValue != null) {
				String gsCreatorTool = "CreatorTool";
				mXMP.setProperty(XMPConst.NS_XMP, gsCreatorTool, msCreatorToolValue);
			}
		}
	}
	
	// Set Producer
	public void	setProducer(String sProducer, boolean bForceApply /*=false */) throws XMPException {
		msProducerValue = sProducer;
		if (bForceApply) {
			if (msProducerValue != null) {
				String gsProducer = "Producer";
				mXMP.setProperty(XMPConst.NS_PDF, gsProducer, msProducerValue);
			}
		}
	}

	// Add PDF/A-specific data.
	public void	putPDFAdata (int iPart, String sConformance, String sAmendment) {
		miPart = iPart;
		msConformance = sConformance;
		msAmendment = sAmendment;
	}	

	private void metadata_helper(Node node) throws XMPException {
		if (node instanceof Element) {
			final Element element = (Element)node;
		
			if (element.isSpecified(XFA.DESCTAG, true, 0))
				processDesc(element);
		}
		
		for (Node child = node.getFirstXMLChild(); child != null; child = child.getNextXMLSibling()) {
			metadata_helper(child);
		}		
	}
	
	private void processPDFAVersionInfo() throws XMPException {
		// part
		final String gsPart = "part";
		mXMP.setPropertyInteger(XMPConst.NS_PDFA_ID, gsPart, miPart);
		
		// conformance
		final String gsConformance = "conformance";
		mXMP.setProperty(XMPConst.NS_PDFA_ID, gsConformance, msConformance);
		
		// amd
		if (!StringUtils.isEmpty(msAmendment)) {
			final String gsAmd = "amd";
			mXMP.setProperty(XMPConst.NS_PDFA_ID, gsAmd, msAmendment);
		}
	}
	
	private static byte[] getXML(Element element) {
		// save to a jfStreamFile-derived class, and then convert that stream into a string
		final ByteArrayOutputStream memStream = new ByteArrayOutputStream();
		
		final DOMSaveOptions options = new DOMSaveOptions();
		options.setExcludePreamble(true);
		element.saveXML(memStream, options);

		return memStream.toByteArray();
	}
	
	private boolean setImageProperty (
			String sPropNameSpace,
			String sPropName,
			String sImageContentType,
			String sImageEncoding,
			String sImageContents) throws XMPException {
		
		if (!sImageEncoding.equals("base64"))
			return false;	// Only base64 encoding currently supported, since that's what XMP expects.
							// We could theoretically re-encode other encodings as base64.

		String sAdjustedContentType = sImageContentType;
		if (sAdjustedContentType.startsWith("image/"))
			sAdjustedContentType = sAdjustedContentType.substring("image/".length());	// remove image/
		if (!sAdjustedContentType.equals("jpeg"))
			return false;	// Unsupported image type.  XMP only supports JPEG.

		// "Thumbnail" code provided by Alan Lillich, who admits we really need an
		// AddThumbnail utility in the XMP library.

		mXMP.appendArrayItem(XMPConst.NS_XMP, "Thumbnails", new PropertyOptions(PropertyOptions.ARRAY_ALTERNATE), null, new PropertyOptions(PropertyOptions.STRUCT));
		final String itemPath = XMPPathFactory.composeArrayItemPath("Thumbnails", XMPConst.ARRAY_LAST_ITEM);
		mXMP.setStructField(XMPConst.NS_XMP, itemPath, XMPConst.TYPE_IMAGE, "format", "JPEG" );
		mXMP.setStructField(XMPConst.NS_XMP, itemPath, XMPConst.TYPE_IMAGE, "image", sImageContents);
		return true;
	}
	
	/**
	 * Add custom info, as in the Acrobat.DocumentProperties>Custom tab
	 * (Stored under xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/"> namespace)
	 * @param sName tag name
	 * @param sValue tag value
	 * @throws XMPException
	 */
	public void addCustomInfo(String sName, String sValue) throws XMPException {
		//
		// From "Embedding XMP Metadata in Application Files"
		// Users of PDF are permitted to define their own metadata items in the PDF Info dictionary.
		// There is another schema whose namespace URI is 'http://ns.adobe.com/pdfx/1.3/',
		// usually given the namespace prefix 'pdfx', which is used to store any such user-defined keys.
		//
		assert(!StringUtils.isEmpty(sName));
		if (!StringUtils.isEmpty(sName)) {
			String sNonConstValue = sValue;
			mXMP.setProperty(XMPConst.NS_PDFX, sName, sNonConstValue);
		}
	}

	/**
	 * Retrieve custom info, given the name.
	 * Returns The value if a name/value pair was found.
	 * Returns null if no such name/value pair was found
	 * @param sPropName property name
	 * @return the value if a name/value pair was found otherwise null
	 * @throws XMPException
	 */
	public String getCustomInfo(String sPropName) throws XMPException {
		//
		// From "Embedding XMP Metadata in Application Files"
		// Users of PDF are permitted to define their own metadata items in the PDF Info dictionary.
		// There is another schema whose namespace URI is 'http://ns.adobe.com/pdfx/1.3/',
		// usually given the namespace prefix 'pdfx', which is used to store any such user-defined keys.
		//
		assert(!StringUtils.isEmpty(sPropName));
		if (!StringUtils.isEmpty(sPropName))
			return mXMP.getProperty(XMPConst.NS_PDFX, sPropName).toString();
		return null;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy