Search code examples
javascriptextendscriptecmascript-3

How do I pass an object to Adobe ExtendScript from Javascript (Adobe CEP)?


I'm working with Adobe CEP (it lets developers create windowed extensions for Adobe CC products). The bulk of my code is modern JavaScript (the platform uses Chromium 57, Node.js 7.7.4). However, in order to access the DOM, I need to write some functions in Adobe ExtendScript and execute them from normal JS. The only way is to execute a script using their provided csInterface.evalScript(script, callback). script has to be a string, which in my case is a function call converted to a string. I want to be able to pass an object to and from ExtendScript via evalScript, but evalScript only takes and returns a string.

Currently, I am passing each object property as its own argument. This is unwieldy, but it works.

My first though was JSON.stringify(), but unfortunately ExtendScript is a dialect of ECMAScript 3, which means no JSON.parse() support.

I can't just concat the object argument into the script function call, because then the string evaluates to foo([object Object]).

I've seen there are functions like eval()/uneval() or Object.toSource(), but those are not supported by Chromium.

Here's an example, similar to my current method:

functions.js (ES3/ExtendScript)

function drawCircle(x, y, name) {
    // pick a layer
    var layer = app.activeDocument.layers[0];

    var diameter = 10;
    var top = y + diameter / 2;
    var left = x - diameter / 2;

    // draw ellipse in layer
    var circle = layer.pathItems.ellipse(top, left, diameter, diameter);

    circle.name = name;
    circle.filled = true;

    return true;
}

app.js (ES6)

const csInterface = new CSInterface();    // provided by Adobe
async function circle() {
    const dataObject = {x: 10, y: 10, name: 'Hello world!'};

    // the script to call
    // evaluates to drawCircle(10,10,'Hello world!');
    const script = "drawCircle(" + dataObject.x + "," + dataObject.y + ",'" + dataObject.name + "');";

    return new Promise((resolve, reject) => {
        csInterface.evalScript(script, (result) => {
            resolve(result);
        });
    });
}

As expected, circle() calls drawCircle() just fine, and an ellipse appears in the document I'm working on. However, executing a script/calling a function by concatenation feels very wrong. So in summary,

  1. I would like some (neater) way of turning dataObject into a string and passing it to drawCircle() via evalScript(),
  2. and I would like to return dataObject from drawCircle() and receive it back as an object. Currently, returning an object only results in "[object Object]" as a return value.

Solution

  • Javascript -> ExtendScript

    The only way to pass objects from Javascript to ExtendScript is to send it as a JSON string with JSON.stringify().

    Yes, you are right about no JSON.parse() support, however, you don't need to.

    You can still send the stringified object and will arrive to ExtendScript as an object.

    const dataObject = {x: 10, y: 10, name: 'Hello world!'};
    const script = "drawCircle(" + JSON.stringify(dataObject) + ")";
    

    And then in ExtendScript, you can get way with doing something like this:

    function drawCircle(obj) {
      var layer = app.activeDocument.layers[0];
    
      var radius = 10;
      var top = obj.y + 5;
      var left = obj.x - 5;
    
      var circle = layer.pathItems.ellipse(top, left, radius, radius);
    
      circle.name = obj.name;
      circle.filled = true;
    
      return true;
    }
    

    ExtendScript -> Javascript

    You will need this ExtendScript module, copied in the same folder as your jsx

    Link to Indiscripts ExtendScript JSON module

    Then include it with #include 'json.jsx'; (or //@include 'json.jsx' to avoid linter errors) in the top of your jsx. This adds a JSON global function that provides two methods: JSON.eval() and JSON.lave().

    The method we need is lave() which allows you to stringify the object back to Javascript. Consider it a friendlier version of JSON.stringify().

    function drawCircle(obj) {
      var layer = app.activeDocument.layers[0];
    
      var radius = 10;
      var top = obj.y + 5;
      var left = obj.x - 5;
    
      // draw ellipse in layer
      var circle = layer.pathItems.ellipse(top, left, radius, radius);
    
      circle.name = obj.name;
      circle.filled = true;
    
      return JSON.lave(circle);
    }
    

    Then in javascript you can parse to an object yet again:

    const dataObject = {x: 10, y: 10, name: 'Hello world!'};
    const script = "drawCircle(" + JSON.stringify(dataObject) + ")";
    
    csInterface.evalScript(script, (result) => {
      console.log(JSON.parse(result));
    });
    

    I tested this in the latest CEP Runtime version (v9).