3

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.
Jason Weinzierl
  • 342
  • 1
  • 6
  • 12

1 Answers1

8

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).

Harry
  • 96
  • 1
  • 5
  • 1
    Thanks, had no idea about being able to pass stringified objects directly! I also found a more general [JSON polyfill](https://bestiejs.github.io/json3/). Both methods modify `$.global`, so I had to restart Illustrator to clear out `JSON.stringify()` before being able to use `JSON.lave()`. – Jason Weinzierl Apr 15 '19 at 05:53
  • If you have special characters inside the string you send, you need to ``escape`` them. If you only use ``decodeURIComponent``, it will not be covering some characters. Especially if you need to support older versions, like Premiere 2019 for example... (I know that ``escape`` is deprecated) – Guntram Oct 13 '21 at 13:07