com.adobe.xfa.xmp.XMPHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aem-sdk-api Show documentation
Show all versions of aem-sdk-api Show documentation
The Adobe Experience Manager SDK
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;
}
}