Photoshop UXP BatchPlay For Beginners

I’m not sure if I am only own here or maybe there are others out there in the same position so I guess this could be classed as a request more than anything.

From what I understand, the API for the UXP Plugins is an ongoing project and as time moves forward more will be added.

In the meantime, from the Docs I have read, we are been encouraged to use the BatchPlay method for filling in the gaps to the API which are not currently available.

Also from my own research, the best tool we have to inspect Photoshop is the Alchemist Plugin which can be used to Generate Code from various tasks we do in Photoshop as well as Inspecting different parts of Photoshops activities and active documents etc.

BatchPlay Code Examples
What I would personally like to see and again, there maybe others out there in the same position who would like to see some code examples on how to use Alchemist along with BatchPlay in order to further understand this new UXP system.

Scenario
Here is a typical scenario which I would like to perform but has got me completely baffled due to the level I am at.

From the Active Document in Photoshop, using the Alchemist Plugin, generate the code and return the color space the active document is in which could either be RGB or Grayscale

Thanks
Ian

3 Likes

The whole BatchPlay approach is definitely much more self-explaining if you already had contact with Action Manager code in CEP panels, as it’s just like a better version of it with less required syntax.
So don’t get discouraged, the beginning of learning it is difficult but once the concept get’s more clearer you see a lot of recurring patterns. Essentially, you send directions to Photoshop (for example set or get some objects/property) that get assigned to a specific element like the active Document or the layer at index 23 etc. (that’s the _ref part or the BatchPlay object)

I agree that more code examples would be helpful for certain scenarios, I’m sure now that UXP is public there will be a lot more examples and open-source repos coming in the next weeks & months.

For your specific scenario, it’s helpful to just inspect the available properties in Alchemist. You can either browse through the property dropdown and see what properties are there or add the descriptor for the whole document (without a property selected) and browse through the tree-content in the main view on the right.

You can click the pin icon in this view to pin a single property. If you go to the generated code, it will respect the pin and adjust the code:

const batchPlay = require("photoshop").action.batchPlay;

const result = await batchPlay(
[
   {
      "_obj": "get",
      "_target": [
         {
            "_ref": "document",
            "_id": 271
         }
      ],
      "_options": {
         "dialogOptions": "dontDisplay"
      }
   }
],{
   "synchronousExecution": false,
   "modalBehavior": "fail"
});
const pinned = result[0].mode;

If you already select the property from the dropdown, only the mode property will be fetched:

const batchPlay = require("photoshop").action.batchPlay;

const result = await batchPlay(
[
   {
      "_obj": "get",
      "_target": [
         {
            "_property": "mode"
         },
         {
            "_ref": "document",
            "_id": 271
         }
      ],
      "_options": {
         "dialogOptions": "dontDisplay"
      }
   }
],{
   "synchronousExecution": false,
   "modalBehavior": "fail"
});

The results are the same, but the second approach has better performance since not all other document properties will be fetched, only the mode property.
If you log the result from the above code, you will see that it’s an array of objects (descriptors), where the first has the property mode:


So you can’t use it as is, instead you get the mode via
result[0].mode

Also note, that the generated Code references the document by its ID. If you want to reuse the code, you should alter this part to reference the active document, otherwise your code will only work on this specific document. Also, for getters it’s fine to set the synchronousExecution to true, because you’ll want to wait for the result anyway:

function getDocumentMode() {
    const batchPlay = require("photoshop").action.batchPlay;
    return batchPlay(
      [
         {
            "_obj": "get",
            "_target": [
               {
                  "_property": "mode"
               },
               {
                  "_ref": "document",
                  "_enum": "ordinal",
                  "_value": "targetEnum"
               }
            ],
            "_options": {
               "dialogOptions": "dontDisplay"
            }
         }
      ],{
         "synchronousExecution": true,
         "modalBehavior": "fail"
      })[0].mode;
  }

Many properties in Photoshop don’t just come as the value, instead it’s incapsulated in an
{
_enum: …
_value: …
}

object.

So here’s an example of how to log the mode as a string:
console.log(getDocumentMode()._value)

3 Likes

Simon, this is excellent and I would like to say thanks for taking the time to explain this step by step and I am sure other people are going to find this useful.

I shall grab a coffee and study everything you have wrote here and then attempt to put it into practice.

Got this working now thanks Simon.
I now have a better understanding on how to use Alchemist and hopefully, after some more experimenting, I can move forward now.

1 Like

You’re welcome, great that you got it working. Sometimes it’s better when the copied code doesn’t work right away, this makes you analyse and understand it much better :nerd_face:

2 Likes

How do you reference the active document?

With

{
    "_ref": "document",
    "_enum": "ordinal",
    "_value": "targetEnum"
}

(usually inside the _target array)

Wow, first of all thank you so much for responding to my question! I haven’t been able to get any help regarding UXP questions on this forum or on the internet in general. Maybe UXP is still too new. Anyway, would you be so kind as to take a look at this script and let me know what I’m doing wrong? I thought it was the document id that was causing the issue but even changing to include what you shared I still get no response. Not sure what I’m doing wrong. Been working on this for days. Thanks in advance!

var PhotoshopCore = require(‘photoshop’).core;

async function test() {

// script has completed.
await PhotoshopCore.showAlert({ message: ‘begin’})

const batchPlay = require(“photoshop”).action.batchPlay;

const result = await batchPlay(
[
{
_obj: “placeEvent”,
null: {
_kind: “local”,
_path: “D:\Projects\Personal\test\bottle.png”
},
“_target”: [
{
“_property”: “mode”
},
{
“_ref”: “document”,
“_enum”: “ordinal”,
“_value”: “targetEnum”
}
],
replaceLayer: {
_obj: “placeEvent”,
from: [
{
_ref: “Layer 1”

        }
     ],
     to: [
        {
           _ref: "Layer 3"

        }
     ]
  },
  _options: {
     dialogOptions: "dontDisplay"
  }

}
],{
synchronousExecution: false,
modalBehavior: “wait”
});

}

document.getElementById(“btnImportImage”).addEventListener(“click”, test);`

UXP is more restrictive in terms of file access. You can’t just access “D:\Projects\Personal\test", instead you need to generate a token and set that as the _path value. A folder like that might require the user to pick it via a file picker (explicitly grant access). You can also load files from the plugin or data folder without showing a file picker. The docs should have a page about that.

Thanks again for the reply. So, I have been able to successfully create a plugin that generates a token by
invoking a dialog and having the user select an image file. Then the user can create/directly import that image file (without the dialog being invoked) by setting the path = to the Token in the Place command.

This all works great. Code I’m using is below.

But here is the issue:

When I look in the console for the output of the console.log for //STORED FILE NAME (see below)
it appears to be identical to the output for the console.log for //DIALOG FILE NAME (see below)

So I at first thought I could dispense with filename = await fs.getFileForOpening(); (to avoid the dialog)
and instead just set filename = to a hard coded string that was identical to the string that was shown in the console for the filename value returned by fs.getFileForOpening();

However, the code does not work (despite no error message/no message at all in the debug screen)

I’m assuming this has to do with the fact that fs.getFileForOpening() returns a Type of File
and not a Type of String?

Any suggestions as to how to use the hard coded filename to make it work like the filename that is returned by fs.getFileForOpening(); ?

Thanks!

CODE***************
const fs = require(“uxp”).storage.localFileSystem;

let entry;

async function getToken() {

const filePath = String.rawC:\\g.png;
var filename = ‘{“name”:“g.png”,“type”:“file”,“nativePath”:"’ + filePath +‘"}’;
console.log("stored filename " + filename); //STORED FILE NAME

filename = await fs.getFileForOpening();
console.log("dialog filename " + filename); //DIALOG FILE NAME

let token = await fs.createPersistentToken(filename);
console.log("token " + token);

document.getElementById(“myfile”).value = filename.name;
document.getElementById(“tok”).value = token;
localStorage.setItem(“persistent-file”, token);

entry = await fs.getEntryForPersistentToken(token);
console.log("entry " + entry);

}

async function imagePlacer()
{
const myToken = await fs.createSessionToken(entry);
console.log("myToken " + myToken);

let result;
let psAction = require(“photoshop”).action;

let command = [
// Place
{“ID”:3,“_obj”:“placeEvent”,“freeTransformCenterState”:{“_enum”:“quadCenterState”,“_value”:“QCSAverage”},“null”:{“_kind”:“local”,“_path”:myToken},“offset”:{“_obj”:“offset”,“horizontal”:{“_unit”:“pixelsUnit”,“_value”:0.0},“vertical”:{“_unit”:“pixelsUnit”,“_value”:0.0}},“replaceLayer”:{“_obj”:“placeEvent”,“from”:{“_id”:2,“_ref”:“layer”},“to”:{“_id”:3,“_ref”:“layer”}}}
];
result = await psAction.batchPlay(command, {});
}

async function newtest()
{
await require(“photoshop”).core.executeAsModal(imagePlacer, {“commandName”: “Action Commands”});
}

document.getElementById(“btnGetToken”).addEventListener(“click”, getToken);
document.getElementById(“btnImportImage”).addEventListener(“click”, newtest);

could you format your code so that it’s more readable?

No problem. See below. Again. the code works. The only issue is how to convert the hard-coded string filename path into Type File/Entry so that fs.createPersistentToken() accepts it as a param.

Thanks again!

const fs = require(“uxp”).storage.localFileSystem;

let entry;

async
function getToken() {

	const filePath = String.rawC:\\g.png;
	var filename = ‘ {“name”: “g.png”,
		“type”: “file”,
		“nativePath”: "’ + filePath +‘"
	}’;
	console.log("stored filename " + filename); //STORED FILE NAME

	filename = await fs.getFileForOpening();
	console.log("dialog filename " + filename); //DIALOG FILE NAME

	let token = await fs.createPersistentToken(filename);
	console.log("token " + token);

	document.getElementById(“myfile”).value = filename.name;
	document.getElementById(“tok”).value = token;
	localStorage.setItem(“persistent - file”, token);

	entry = await fs.getEntryForPersistentToken(token);
	console.log("entry " + entry);

}

async
function imagePlacer() {
	const myToken = await fs.createSessionToken(entry);
	console.log("myToken " + myToken);

	let result;
	let psAction = require(“photoshop”).action;

	let command = [
	// Place
	{“ID”: 3,
		“_obj”: “placeEvent”,
		“freeTransformCenterState”: {“_enum”: “quadCenterState”,
			“_value”: “QCSAverage”
		},
		“null”: {“_kind”: “local”,
			“_path”: myToken
		},
		“offset”: {“_obj”: “offset”,
			“horizontal”: {“_unit”: “pixelsUnit”,
				“_value”: 0.0
			},
			“vertical”: {“_unit”: “pixelsUnit”,
				“_value”: 0.0
			}
		},
		“replaceLayer”: {“_obj”: “placeEvent”,
			“from”: {“_id”: 2,
				“_ref”: “layer”
			},
			“to”: {“_id”: 3,
				“_ref”: “layer”
			}
		}
	}];
	result = await psAction.batchPlay(command, {});
}

async
function newtest() {
	await require(“photoshop”).core.executeAsModal(imagePlacer, {“commandName”: “Action Commands”
	});
}

document.getElementById(“btnGetToken”).addEventListener(“click”, getToken);
document.getElementById(“btnImportImage”).addEventListener(“click”, newtest);

I don’t really see the issue, you’re already calling getFileForOpening(), which returns a File/Entry, doesn’t it?

Yes, getFileForOpening() does return a File/Entry and the the code works.
But I don’t want to use getFileForOpening() because it prompts the user with a dialog intially.

I want to use the hard-coded variable that I’m indicating as STORED FILE NAME in the comments instead (see below).

However, if I comment out
the filename that is returned from the getFileForOpening() function and instead
use filename that is set equal to the hard coded path (STORED FILE NAME) and use that as the param for fs.createPersistentToken(), it won’t work because in this case filename would be a string not a File/Entry.

My question is how can I convert the hard-coded filename string into a File/Entry that I can pass to as a param to fs.createPersistentToken()?


const filePath = String.rawC:\\g.png;
	var filename = ‘ {“name”: “g.png”,
		“type”: “file”,
		“nativePath”: "’ + filePath +‘"
	}’;
	console.log("stored filename " + filename); //STORED FILE NAME

	//filename = await fs.getFileForOpening();
	//console.log("dialog filename " + filename); //DIALOG FILE NAME

	let token = await fs.createPersistentToken(filename);

So you need a token anyway. To get a token, you need an entry. To get an entry, you have two options:

  • Get it from plugin/data folder without picker
  • Get it from anywhere else with a picker

As Simon said, there’s no other option in UXP

P. S. How does this work? String.rawC:\\g.png
Is it a valid syntax in JS?