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

1 Like

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

6 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

Hello, I modified the XMP data of the document according to your method, but it was not successfully modified :weary:

1 Like

Your solution work for me until set the metadata with the batchplay code. It didn’t edit the metadata and didn’t return any error. Do you have any ideas to why? I change the code a bit

const bp = require("photoshop").action.batchPlay;
const getDocumentXMP = () => {
  return bp([{
    _obj: "get",
    _target: {
      _ref: [
        { _property: "XMPMetadataAsUTF8" },
        { _ref: "document", _enum: "ordinal", _value: "targetEnum" }
      ]
    }
  }], { synchronousExecution: true })[0].XMPMetadataAsUTF8
}
const setDocumentXMP = (xmpString) => {
  bp([{
    _obj: "set",
    _target: [
      { _ref: "property", _property: "XMPMetadataAsUTF8" },
      { _ref: "document", _enum: "ordinal", _value: "targetEnum" }
    ],
    to: {
      _obj: "document",
      XMPMetadataAsUTF8: xmpString
    }
  }], {});
}
const getDocumentMetadata = (key, prefix) => {
  const xmpString = getDocumentXMP();
  const obj = convert(xmpString, { format: "object" });
  return obj["x:xmpmeta"]['rdf:RDF']['rdf:Description'][`${prefix}:${key}`] || "";
}
const setDocumentMetadata = (key, value, prefix) => {
  const xmpString = getDocumentXMP();
  const obj = convert(xmpString, { format: "object" });
  obj["x:xmpmeta"]['rdf:RDF']['rdf:Description'][`${prefix}:${key}`] = value;
  const newXmpString = convert(obj, { format: "xml" })
  setDocumentXMP(newXmpString);
}
try {
  setDocumentMetadata("CreateDate", "1", "xmp");
} catch (error) {
  console.log(error.message);
}

I test the same code, and it didn’t work for me as well: it would be great to get some assistance on this special for adobe team @Erin_Finnegan . I am trying to set the plus:Licensor and Web Statement.

    <plus:Licensor>
            <rdf:Seq>
               <rdf:li rdf:parseType="Resource">
                  <plus:LicensorName>Text</plus:LicensorName>
                  <plus:LicensorID>text</plus:LicensorID>
                  <plus:LicensorCity>text</plus:LicensorCity>
                  <plus:LicensorRegion>text</plus:LicensorRegion>
                  <plus:LicensorCountry>text</plus:LicensorCountry>
                  <plus:LicensorEmail>text@url.com</plus:LicensorEmail>
                  <plus:LicensorURL>www.url.com</plus:LicensorURL>
               </rdf:li>
            </rdf:Seq>
         </plus:Licensor>

and Web Statement properties:

      /<xmpRights:WebStatement>text</xmpRights:WebStatement>/

These properties are not recordable with the action panel, the only way is via XML manipulation. Any help would be appreciated. Thanks!

Maybe @Sujai can help?

Recently InDesign added UXP support, and we have an XML-related “recipe” here: https://developer.adobe.com/indesign/uxp/resources/recipes/xml/

I’m not sure how I could help beyond this.

Handling XMP using the UXP XMP module will be available in UXP v7.2.
I’m hoping that Photoshop will enable this feature for the 25.0 release. Will share more details on this in a few weeks once I have confirmation.

6 Likes

Is it available now?

Yes it is… as would reveal a search on the forum or a look at the changelog. :wink:

.

1 Like