Document XMP metadata

Hi,
I’m trying to get, set and delete a namespaced key, value pair on the XMP at the document’s level. I’ve borrowed @simonhenke code from this post on Layer XMP, and edited a bit to transform it on Document XMP. I’m using xmldom, and I’ve got this:

const bp = require("photoshop").action.batchPlay
const { DOMParser, XMLSerializer, DOMImplementation } = require('xmldom');

// Get all the Doc XMP
const getDocumentXMP = () => {
  return bp([{
    _obj: "get",
    _target: {
      _ref: [
        { _property: "XMPMetadataAsUTF8" },
        { _ref: "document", _enum: "ordinal", _value: "targetEnum" }
      ]
    }
  }], { synchronousExecution: true })[0].XMPMetadataAsUTF8
}

// Set all the Doc XMP
const setDocumentXMP = (xmpString) => {
  bp([{
    _obj: "set",
    _target: [
      { _ref: "property", _property: "XMPMetadataAsUTF8" },
      { _ref: "document", _enum: "ordinal", _value: "targetEnum" }
    ],
    to: {
      _obj: "document",
      XMPMetadataAsUTF8: xmpString
    }
  }], {});
}

// Get key of namespace
export const getDocumentMetadata = ({ key, namespace }) => {
  const xmpString = getDocumentXMP();
  if (xmpString) {
    const xmlDoc = new DOMParser().parseFromString(xmpString, 'text/xml');
    const node = namespace ? xmlDoc.getElementsByTagNameNS(namespace, key)[0] : xmlDoc.getElementsByTagName(key)[0]
    if (node) {
      return node.textContent
    }
  }
}

// set key, value of namespace
export const setDocumentMetadata = ({ key, value, namespace }) => {
  const xmpString = getDocumentXMP();
  const xmlDoc = xmpString ? new DOMParser().parseFromString(xmpString, 'text/xml') : new DOMImplementation().createDocument(namespace, key, null);
  const node = namespace ? xmlDoc.getElementsByTagNameNS(namespace, key)[0] || xmlDoc.createElementNS(namespace, key) :
    xmlDoc.getElementsByTagName(key)[0] || xmlDoc.createElement(key);
  node.textContent = value;
  xmlDoc.appendChild(node);
  console.log(xmlDoc); // the document does contain the new node
  const newXmpString = new XMLSerializer().serializeToString(xmlDoc); // <= something wrong here...?
  console.log("oldXmpString", xmpString);    // Old XMP string
  console.log("newXmpString", newXmpString); // New XMP string... they're equal :-/
  setDocumentXMP(newXmpString)
}

// delete key of namespace
export const deleteDocumentMetadata = ({ key, namespace }) => {
  const xmpString = getDocumentXMP();
  const xmlDoc = new DOMParser().parseFromString(xmpString, 'text/xml');
  const node = namespace ? xmlDoc.getElementsByTagNameNS(namespace, key)[0] : xmlDoc.getElementsByTagName(key)[0]
  if (node) {
    xmlDoc.removeChild(node)
    setDocumentXMP(new XMLSerializer().serializeToString(xmlDoc))
  }
}

// TEST
setDocumentMetadata({key: "someKey", value: "someValue", namespace: "undavide:"})
var newXMP = getDocumentMetadata({key: "myKey", namespace: "undavide:"});
console.log({newXMP}) // undefined

Which in theory should work, in practice it doesn’t. The test sets a namespaced key, value pair, then retrieves it but it gets through as undefined.

I think I’ve been able to pinpoint the problem in the setDocumentMetadata() function. The new node is successfully created and also appended to the xmlDoc, as the log in the Console shows. For some reason, though, the XMLSerializer() produces a newXmpString that is perfectly equal to the original one, that is to say without the new node. I’m logging them both. From that point onwards, the subsequent call to setDocumentXMP() is pointless, because the new xml string lacks the additional node.

Can your fresh pairs of eyes spot any issue in the above code?
Thanks!!

Davide

Further inspection shows that I was wrong in saying that the two strings are equal, the console logs a long series of carriage returns that I thought were pointless:

Instead, at the very end there is the custom namespaced tag:

Although it is outside the <?xpacket end="w"?> tag, i.e. not reachable when you later try to get it :neutral_face:

It still doesn’t work, although I’ve a clearer idea on the reason why…

Solved :wine_glass:
I had to use a different XML library called xmlbuilder2.

const { convert } = require('xmlbuilder2');

// GETTER
// -------------------------------------------------
// <rdf:Description xmlns:undavide="http://davidebarranca.com/notes/" etc...>
// <undavide:Notes>someValue</undavide:Notes>
// -------------------------------------------------
// key: Notes
// prefix: undavide
// namespace: http://davidebarranca.com/notes/

export const getDocumentMetadata = ({ key, prefix = 'undavide', namespace = 'http://davidebarranca.com/notes/' }) => {
  const xmpString = getDocumentXMP();
  const obj = convert(xmpString, { format: "object" });
  return obj["x:xmpmeta"]['rdf:RDF']['rdf:Description'][`${prefix}:${key}`] || "nothing";
}

// SETTER
// -------------------------------------------------
// <rdf:Description xmlns:undavide="http://davidebarranca.com/notes/" etc...>
// <undavide:Notes>someValue</undavide:Notes>
// -------------------------------------------------
// key: Notes
// value: someValue
// prefix: undavide
// namespace: http://davidebarranca.com/notes/

export const setDocumentMetadata = async ({ key, value, prefix = 'undavide', namespace = 'http://davidebarranca.com/notes/' }) => {
  const xmpString = getDocumentXMP();
  const obj = convert(xmpString, { format: "object" });
  obj["x:xmpmeta"]['rdf:RDF']['rdf:Description'][`@xmlns:${prefix}`] = namespace;
  obj["x:xmpmeta"]['rdf:RDF']['rdf:Description'][`${prefix}:${key}`] = value;
  const newXmpString = convert(obj, {format: "xml"})
  await setDocumentXMP(newXmpString);
}

Might need a bit of extra testing for edge cases. What fun XML is, who would have thought :slight_smile:
If you run the prerelease version of PS see my post in the forums there for details on the BatchPlay call.

Davide

3 Likes

Hi Davide,

Do you have any resources for xmp manipulation similar to what was possible with extendscript?
I tried incorporating some of the code above but couldn’t get it working in my scenario.

I have created several CEP panels and scripts that display and interact with custom XMP metadata with in Photoshop. I’ve searched this forum and I have not found an easy way to do this via UXP plug ins.
I used to be able to retrieve this metadata easily and append it in extendscript:

var docRef = app.activeDocument;
var xmp = new XMPMeta(activeDocument.xmpMetadata.rawData);
var customField = xmp.getProperty(“http://my.custom.namespace/”, “Custom”);
xmp.setProperty(“http://my.custom.namespace/”, “Custom”, “Test”);
docRef.xmpMetadata.rawData = xmp.serialize(XMPConst.SERIALIZE_USE_COMPACT_FORMAT);

Is there an easy way to do this in the UXP API or BatchPlay?

Thanks