BatchPlay, Layer Visibility, Nested Smart Objects, Bitmap Halftone, Saving & Closing, Oh my!

hey, if it works it works!
I’d say publish the first version as long as it is tested well enough.

batchPlay is great for performance and is essential if you want to do everything, since DOM support is still limited.
I advised you to avoid it since it’s harder to debug, understand and tweak. but if you have a promising code that works, go with it.

1 Like

batchPlay is essentially as bare metal you can get without going down the C++ route. It’s faster than the API, and at this point is the only way to access many of Photoshop’s features until the API is complete.

All my plugins are essentially composed of many small helper functions that encapsulate a single bit of batchPlay - effectively my own API.

My intuition is screaming exactly this after even more reading. (Honestly, two days of wall to wall reading, watching, note taking and practice with small stuff after this lovely initial kick in the ass from everyone :muscle:)

I’m just really having a hard time wrapping my mind around understanding how to accomplish editing a smart object, saving it and closing it to return to the original document using tokens.

Reading through some threads (thanks! @Maher) I now understand that tokens are just strings. ( I assume like strings/data-snippets tied to UI Buttons in Alchemist code… like “license plates to cars”)

Saving using tokens and persistent vs temp storage (my head hurts) being the most difficult aspect by far.

1 Like

Ok, so forgive me if any of this is teaching you to suck eggs!

In the early days of UXP there was no arbitrary file acces and the token system was the only method of accessing files. The intention was to disallow plugins from gaining access to the filesystem without user consent. There is now the ability for arbitrary access but let’s ignore that for now.

A token is essentially a key/value pair of a path and a unique ID that PS holds in memory that says “User has granted permission to access this file/folder”. There are two types of token - Session, which is temporary and lasts for the duration of that PS session, and Persistent, which will be available in all PS sessions in the future. To use a persistent token in the future you’re going to need to save it somewhere (a JSON file, a database, etc) so you can access it - you can’t “get” it from PS later.

In the case of a folder, a token also provides access to any descendents.
A new, unsaved document doesn’t have a token/entry as it purely exists in memory until it is saved.

The only way to generate a token is via a file picker using one of the File System Provider methods.

How you handle that depends on your use case, desired UX, and general preference.
One approach would be to use a file picker every time you need to create a token, another would be to have users grant access to a dedicated working folder once using a persistent token (for example in a settings page/dialog) and then all your code is targeted to that folder.

Once we have a token we can use it to get an entry with the relevant getEntryFor...Token method (see previous link) that then allows us to interact with the file using the various entry methods, although not always - e.g. saveAs is passed a token and not an entry.

In a nutshell , we don’t really interact with the file itself much, we create a token and then interact with the entry.

So, to consider your use case:
I’m going to make two assumptions - 1) The master file is new and has never been saved 2) the smart object will be created programmatically and as such will not have been saved either.

The user reaches the point where they have opened and edited the smart object and via some undetermined mechanism indicates that they are ready to save and close the smart object - at this point your code is going to do something like this:
• Generate token via user input/file picker (for a folder to save in)
• With token, get entry for folder
• Create a file entry within folder
• Create token for file
• Write smart object to file with token
• Close smart object

As you now have a token/entry for a folder any subsequent operations can use it (more smart objects, saving the master file, etc).

That’s a super reductive summary and there’s definitely more to consider, especially as you want to cede control to user which can get messy very quickly (what if they create and open a new smart object within the smart object for example).

Here’s a repo I made to demonstrate using a single persistent token to grant permanent access to a working folder. It might be a bit overwhelming, but it contains a lot of the ingredients for what you want, tokens, saving, and batchPlay. It also demonstrates the more functional approach to coding mentioned before.

2 Likes

Arbitrary file access is now available with the new getEntryFromUrl and createEntryFromUrl methods.
They allow you to bypass the user input/file picker, but the rest of the process would be the same.
For example, you could get the active document’s path and pass it to getEntryFromUrl to initiate your process.
There’s a bit more to it than that; you’d need to set the relevant permissions in the manifest.

2 Likes

great work so far! try to not get overwhelmed by the details, coding is like this… suddenly everything will make sense.

as of your question about the smart object, let’s break the issue into manageable parts:

  1. we have an opened document “active document” , so we can store its ID or name in a variable.
  2. we open the smart object (basically opening a document)
  3. we get the ID or name of the newly opened doc and store it

now we have two options:
4a. we activate it and target the "active document " in our edits
or
4b. we get its ID and pass it to our batchplay action descriptors

  1. save and close the 2nd doc
  2. re-activate the original doc

where are you stuck in those steps? I’ll be more than happy to help

2 Likes

hello, back again with some code that might help:

// the function
async function openSmartObject(layerName) {
    let command;
    let myDoc = app.activeDocument
    let docID = myDoc.id
    let layerID = myDoc.layers.getByName(layerName).id
    // Edit Contents
    command = {"_obj":"placedLayerEditContents","documentID": docID,"layerID": layerID};
    await batchPlay([command], {});
    let newDocID = app.documents.find(doc => doc.name.startsWith(layerName)).id
    return newDocID
}

//the call
executeAsModal(async () => {
    //step 1. store current doc ID
    let mainDocID = app.activedocument.id;
    // steps 2-3-4a. open the smart object using layer name and storing the ID of the newly opened doc
    let smartObjectDocID = await openSmartObject("smart_object_layer_name");
    //opened doc gets selected (active) by default but we select by ID just to make sure
    await batchPlay([{"_obj":"select","_target":[{"_ref":"document", "_id": smartObjectDocID }]}], {}))
   //here we do our edits...
   //now we close and save
    //now we select (activate) the original doc using the stored ID
    await batchPlay([{"_obj":"select","_target":[{"_ref":"document", "_id": mainDocID }]}], {}))
});
1 Like

I wouldn’t use executeAsModal like that - it’s putting PS into a modal state and then ceding control back to the user - what if they want to use another plugin to do their edits?
I’d only call it when it’s needed so it doesn’t block anything else.
In your example the only thing I’d wrap in an executeAsModal would be the batchPlay call.

1 Like

generally I’d agree, but since we’re working with multiple docs, we don’t want the user to close docs or modify the state, that’s why I wrap the whole call in a modal state

1 Like

On re-reading the OP I think I’ve got the wrong end of the stick!
I thought @Sullyman wanted to to stop the script and allow the user to edit the smart object document manually, but I think I’ve got that wrong and he wants to perform his own predetermined edits, for which your example makes perfect sense.

1 Like

So, I might’ve got a bit carried away :rofl:
Here’s a full prototype, the only thing I’ve not cracked is getting the smart object edits to update the master document. The file saves, but it doesn’t update, and I’m not sure I’m using placedLayerUpdateAllModified correctly. Perhaps @Maher has some bright ideas as to what’s going on with that!

const { app, core, action } = require("photoshop");
const fs = require("uxp").storage.localFileSystem;
const batchPlay = action.batchPlay;
const executeAsModal = core.executeAsModal;

const makeDefaultDocument = async () => {
  const result = await batchPlay(
    [
      {
        _obj: "make",
        new: {
          _obj: "document",
          artboard: false,
          autoPromoteBackgroundLayer: false,
          preset: "Default Photoshop Size",
        },
      },
    ],
    {}
  );

  return result[0];
};

const makeNewLayer = async (id) => {
  const result = await batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "layer",
          },
        ],
        layerID: id,
      },
    ],
    {}
  );

  return result[0];
};

const convertToSmartObject = async (docId, layerId) => {
  return await batchPlay([{ _obj: "newPlacedLayer" }], {});
};

const openSmartObject = async (layerId) => {
  const result = await batchPlay(
    [
      {
        _obj: "placedLayerEditContents",
        layerID: layerId,
      },
    ],
    {}
  );

  return result[0];
};

const makeSolidColour = async (red, green, blue) => {
  const result = await batchPlay(
    [
      {
        _obj: "make",
        _target: [
          {
            _ref: "contentLayer",
          },
        ],
        using: {
          _obj: "contentLayer",
          type: {
            _obj: "solidColorLayer",
            color: {
              _obj: "RGBColor",
              red: red,
              grain: green,
              blue: blue,
            },
          },
        },
      },
    ],
    {}
  );

  return result[0];
};

const updateSmartObjectLink = async (id, layerId) => {
  const result = await batchPlay(
    [
      {
        _obj: "placedLayerUpdateAllModified",
        documentID: id,
        layerIDs: [layerId],
      },
    ],
    {}
  );

  return result;
};

const saveDocument = async (id, filename, folder) => {
  // Create file
  const file = await folder.createFile(filename, {
    overwrite: true,
  });

  // Generate token
  const token = await fs.createSessionToken(file);

  const result = await batchPlay(
    [
      {
        _obj: "save",
        in: {
          _path: token,
          _kind: "local",
        },
        saveStage: {
          _enum: "saveStageType",
          _value: "saveBegin",
        },
        documentID: id,
      },
    ],
    {}
  );

  return result;
};

const closeDocument = async (id) => {
  const result = await batchPlay(
    [
      {
        _obj: "close",
        saving: {
          _enum: "yesNo",
          _value: "no",
        },
        documentID: id,
      },
    ],
    {}
  );

  return result;
};

const activateDocument = (id) => {
  app.activeDocument = app.documents.find((doc) => doc.id === id);
};

const makeAndOpenSmartObject = async (masterDocId) => {
    // Make new layer
    const newLayer = await makeNewLayer(masterDocId);

    // Convert new layer to smart object
    await convertToSmartObject(masterDocId, newLayer.layerID);

    // Open smart object
    const smartObjectDoc = await openSmartObject(newLayer.layerID);

    return {
      newLayer,
      smartObjectDoc,
    };
};

const main = async () => {
  await executeAsModal(async () => {
    try {
      // Get folder for saving
      const saveFolder = await fs.getFolder();

      // Make master document
      const masterDoc = await makeDefaultDocument();

      // Make and open smart object
      const { newLayer, smartObjectDoc } = await makeAndOpenSmartObject(
        masterDoc.documentID
      );

      // Perform edit to smart object
      await makeSolidColour(0, 0, 255);

      // Generate filename from layer id
      const filename = `SmartObject_${newLayer.layerID}`;

      // Update master doc
      await updateSmartObjectLink(masterDoc.documentID, newLayer.layerID);

      // Save smart object
      await saveDocument(smartObjectDoc.documentID, filename, saveFolder);

      // Close smart object
      await closeDocument(smartObjectDoc.documentID);

      // Activate master document
      activateDocument(masterDoc.documentID);
    } catch (error) {
      console.log("💥", error);
    }
  });
};
1 Like

Wow @Timothy_Bennett @Maher , I’m blown away at your kindness first off. Secondly, I feel terrible because Sundays I tend to be away a bit and then I come back and see this treasure chest of awesome info! :raised_hands:

The information is honestly, incredible and please allow me a little time to digest! I’m really trying to not do copy pasta this and want to understand what is going on. If you can bear with me while I try and understand what is going on line by line on each.

I really feel this is the exam I need to nail or it’s the first trial by fire I have to cross. :nerd_face:

Allow me a little time to pick this apart and try and understand as much as I can before I even think of asking another question. Because, and if you don’t mind, I’m sure I’ll have plenty but want to make sure they’re as poignant as they can be because I know much valuable your time is.

Truly, thank you for sharing it with me I want to make sure I do it justice! Seriously, thank you! :pray:

2 Likes

Have you tried:

await doc.save();
await doc.close();

This allows Photoshop to do all the heavy-lifting since it’s creating a temporary file when you open the contents of the smart object and keeping track of it in the background. There’s no need to specify a folder for saving since Ps already knows to save the opened smart object back to the original document from whence it came when it is saved.

Ok, so I’ve been reading and taking the JS course Timothy Suggested (on to the part 2 of the Fundamentals ::jersey fist pump:: and are coming back for more punishment.

I’m still struggling to hold onto what certain things (code samples everyone is sharing) are coming from the UXP or PS API and understanding the proper syntax but bear with me.

I would say it’s like this:

1. There needs to be some bP “clean up” on the active layer before moving into the smart object creation.
2. The active layer (now cleaned up and what would be the users choice to have “processed” by the plugin) in the active document would then get converted into a smart object.

Now hopefully I understand your question a little better now :sweat_smile:

4a. This would essentially mean, open the new doc (activate?) and then perform bP edits?

or

4b. Avoid the code relying/targeting the active document and instead, use the ID’s of the origin doc and feed them to bP actions?

hopefully I understand that correctly.

I’m not sure what could be the potential pro’s and con’s of either route (4a or 4b). I know the next step would be reliant on changing the docs (smart object) color mode to access the bitmap halftoning functions, then saving and closing. So not sure if that adds any weight to the decision.

As far as being stuck I’ll say I’m chewing gum and everything else is the underside of a highschool desk.:clown_face:

but in all seriousness. I’m definitely starting to read things better, writing them has been difficult because I’m trying to do the fundamentals without looking to better install them mentally. It’s just knowing when something is an arbitrarily named function or object vs an API or JS one.

Thanks again for all the awesome help! :muscle: