Displace Filter not scriptable due to file token limitations?

I’m trying to apply a displace filter but the batchPlay command doesn’t execute.

I’m aware of the file access limitations, so I’ve put the PSDs used for the displace filter into the plugin specific folder, which I can access via FileSystemProvider.getPluginFolder()
I can read all the entries and aggregate the paths, but as mentioned, the command won’t play.

I’ve tried to replay the Descriptor in Alchemist and it doesn’t work there either, it just gives an “Invalid File Token used” error.

Is there any way to get this working with files/paths inside the Plugin folder? I thought that this one and some other folders like the settings folder would be accessible without tokens…
It would be very bad UX if the user would have to select the file via an open-file-dialog every time :slightly_frowning_face:

Edit: The displace filter doesn’t even work when using the open-file-dialog to retrieve the filepath

Alchemist is not a friend with file tokens. It will provide only a hint but then you have to change it. Also, replay won’t work and in dispatch, you have to exactly know what you are doing. And that is not probably going to change anytime soon.

Can you share a snippet of what your batchPlay looks like?

@Jarda: wasn’t a blame on Alchemist, I just meant that it validated to me that my descriptor wasn’t wrong or anything, but that Photoshop can’t execute it in general.

@kerrishotts: Sure, initially my parameterized function looked like this:

export function __displace(horizontalScale: number, verticalScale: number, path: string, 
  displacementMap: DisplacementMap = DisplacementMap.stretchToFit, undefinedArea: UndefinedArea = UndefinedArea.repeatEdgePixels): ActionDescriptor {
  return {
    _obj: 'displace',
    displacementMap: {
      _enum: 'displacementMap',
      _value: displacementMap,
    undefinedArea: {
      _enum: 'undefinedArea',
      _value: undefinedArea,
    displaceFile: {
      _path: path,
      _kind: 'local',

since that didn’t do anything, I also tried to directly use the original Descriptor:

    "_obj": "displace",
    "horizontalScale": 10,
    "verticalScale": 10,
    "displacementMap": {
       "_enum": "displacementMap",
       "_value": "stretchToFit"
    "undefinedArea": {
       "_enum": "undefinedArea",
       "_value": "repeatEdgePixels"
    "displaceFile": {
       "_path": "C:\\Users\\simon\\Desktop\\plugin\\dist\\displacements\\displace-1.psd",
       "_kind": "local"
    "_isCommand": true,
    "_options": {
       "dialogOptions": "dontDisplay"

Same result though. The descriptor was added to photoshop.action.batchplay() as usual.
Do I have to process the path in a way I’m not aware of yet? Or does UXP simply refuse to run anything with a file reference in it?

and here’s the code where I aggregate the file paths (I choose one of the paths when I call the displace filter)

const pluginFolder = await fs.getPluginFolder()
const displaceFolder = await pluginFolder.getEntry('displacements')
const displaceEntries = await displaceFolder.getEntries()
const displacements = displaceEntries.map(e => e.nativePath)

Choosing a file via

const file = await fs.getFileForOpening()
const path = file.nativePath

didn’t work either.

Edit: Okay I’ve got it working now. Totally neglected the whole Token thing as I hadn’t used it yet and there’s no docs on that yet.

Token docs are coming in the next sprint deploy (end of this week).

Also, if you wouldn’t mind sharing your working code for others, that’d be nice :slight_smile:

1 Like

Looking forward to the sprint deploy!

I wasn’t sure about sharing as I found the relevant code only in one of your answers in a prerelease topic, but if that’s fine then here it is:

const { storage } = require("uxp")
const { localFileSystem: fs } = storage
const tokenCache: {[key: string]: any} = {}

public async loadDisplacements() {
  try {
    const pluginFolder = await fs.getPluginFolder()
    const displaceFolder = await pluginFolder.getEntry('displacements')
    const displaceEntries = await displaceFolder.getEntries()
    displaceEntries.forEach(e => {     
      const token = fs.createSessionToken(e)
      tokenCache[e.nativePath] = token
  } catch (e) { console.error(e) }

// ... later on (path is known)
const token = tokenCache[path]

createSessionToken() was the relevant part I was missing. I ended up using a map-like object as a token cache where I store all the tokens for the relevant files, where the path is the key and the token is the value, so that it can be easily accessed later on.


Yeah, the createSessionToken was supposed to be in the documentation released at MAX, but I hadn’t caught that it didn’t get emitted in our internal scripts. But that’s being fixed this week.

Your script above is perfectly fine to share – thanks for doing so! :slight_smile: