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

At the beginning of my journey (no JavaScript knowledge) so forgive me for the eventual mountain of questions that might proceed but hoping to get some help from what lurking, looks like an incredibly helpful community!

I’ve searched the documentation and forum and just can’t seem to put together what I need.

I’m using the Alchemist suite (Thank you Jarda & Others!) and making great progress until I run into walls because of my gaps in knowledge.

I’m trying to create a button within my plugin that will perform an action from a set I made and I’ll describe the function below. But keep in mind I think the button breaks because of several reasons but let me break this down:

Intention:

    1. Duplicate a layer/group
    1. Hide all other layer except the duplicate
    1. Create a smart object (“A”, we’ll call it) out of the duplicate
    1. Edit the newly created smart object (A)
    1. Within the new smart object (A), duplicate the included layer/group
    1. Convert the new duplicate into a smart object (B)
    1. Edit the new smart object (B)
    1. Within the new smart object (B), perform “Image/Mode/Greyscale” then “Image/Mode/Bitmap”
    1. Create a halftone screen using the dialog (Which I want users to eventually place their own input)
    1. Save Smart object (B) and close.
    1. Now within smart object (A) I need to perform some masking and then save and close.
  • Then finish in the original document by rasterizing the currently selected layer (smart object A)

Issues I Think I’m having:
(Mind you, I’m not a JS guy yet, so I might not understand the proper terminolgy)

  1. I think I’m running into an issue when hiding and showing layers, the code is reliant on the actual names of the layers (What I think are ID’s) instead of “object” it’s self. So when it looks to hide layers, it’s looking for arbitrary names a user gives them, instead of the object?
  2. I when saving I’m running into a similar issue like above in where the ID given to a newly created smart object or it’s user provided layer name instead of the object?

I also get an invalid token when trying to accomplish the first save of a smart object (B) when trying to replay the batch of "listened’ steps in Alchemist which is what leads me to think the “IDs” begin generated arbitrarily are the issue.

I think the first two issues are my biggest hurdle to get me to my next stage. I’m sure I’m not using the correct terminology but hopefully someone will understand the concept of what I’m trying to accomplish.

I feel like these two ideas are what are just not landing for me in any explanations I have found so if anyone cares to jump in with some code ideas or learning links I’d be eternally grateful!

// Events recognized as notifiers are not re-playable in most of the cases. There is high chance that generated code won't work.

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;

async function actionCommands() {
   const result = await batchPlay(
      [
         {
            _obj: "duplicate",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            name: "Halftone Results",
            version: 5,
            ID: [
               175,
               176,
               177,
               178
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "hide",
            null: [
               {
                  _ref: [
                     {
                        _ref: "layer",
                        _name: "Layer 0"
                     },
                     {
                        _ref: "layer",
                        _name: "KOB Artwork"
                     },
                     {
                        _ref: "layer",
                        _name: "Blacks"
                     },
                     {
                        _ref: "layer",
                        _name: "KO Black"
                     },
                     {
                        _ref: "layer",
                        _name: "KOB Artwork"
                     },
                     {
                        _ref: "layer",
                        _name: "Blacks"
                     }
                  ]
               }
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "newPlacedLayer",
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "placedLayerEditContents",
            documentID: 60,
            layerID: 179,
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "selectNoLayers",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "show",
            null: [
               {
                  _ref: [
                     {
                        _ref: "layer",
                        _name: "KOB Artwork"
                     },
                     {
                        _ref: "layer",
                        _name: "Blacks"
                     }
                  ]
               }
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "make",
            _target: [
               {
                  _ref: "layerSection"
               }
            ],
            from: {
               _ref: "layer",
               _enum: "ordinal",
               _value: "targetEnum"
            },
            layerSectionStart: 184,
            layerSectionEnd: 185,
            name: "Group 1",
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "set",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            to: {
               _obj: "layer",
               name: "Artwork"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "mergeLayersNew",
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "duplicate",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            name: "Halftones",
            version: 5,
            ID: [
               186
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "select",
            _target: [
               {
                  _ref: "layer",
                  _name: "Artwork"
               }
            ],
            makeVisible: false,
            layerID: [
               184
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "make",
            new: {
               _class: "channel"
            },
            at: {
               _ref: "channel",
               _enum: "channel",
               _value: "mask"
            },
            using: {
               _enum: "userMaskEnabled",
               _value: "transparency"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "delete",
            _target: [
               {
                  _ref: "channel",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "select",
            _target: [
               {
                  _ref: "layer",
                  _name: "Halftones"
               }
            ],
            makeVisible: false,
            layerID: [
               186
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "newPlacedLayer",
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "placedLayerEditContents",
            documentID: 433,
            layerID: 187,
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "convertMode",
            to: {
               _class: "grayscaleMode"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "convertMode",
            to: {
               _obj: "bitmapMode",
               resolution: {
                  _unit: "densityUnit",
                  _value: 1440
               },
               method: {
                  _enum: "method",
                  _value: "halftoneScreen"
               },
               frequency: {
                  _unit: "densityUnit",
                  _value: 45
               },
               angle: {
                  _unit: "angleUnit",
                  _value: 22.5
               },
               shape: {
                  _enum: "shape",
                  _value: "ellipse"
               }
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "imageSize",
            resolution: {
               _unit: "densityUnit",
               _value: 300
            },
            scaleStyles: true,
            constrainProportions: true,
            interfaceIconFrameDimmed: {
               _enum: "interpolationType",
               _value: "nearestNeighbor"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "save",
            in: {
               _path: "C:\Users\Johnny\AppData\Local\Temp\Halftones1.psb",
               _kind: "local"
            },
            saveStage: {
               _enum: "saveStageType",
               _value: "saveBegin"
            },
            documentID: 445,
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "save",
            in: {
               _path: "C:\Users\Johnny\AppData\Local\Temp\Halftones1.psb",
               _kind: "local"
            },
            documentID: 445,
            saveStage: {
               _enum: "saveStageType",
               _value: "saveSucceeded"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "close",
            documentID: 445,
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "set",
            _target: [
               {
                  _ref: "channel",
                  _property: "selection"
               }
            ],
            to: {
               _ref: "channel",
               _enum: "channel",
               _value: "blue"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "inverse",
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "delete",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            layerID: [
               187
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "make",
            new: {
               _class: "channel"
            },
            at: {
               _ref: "channel",
               _enum: "channel",
               _value: "mask"
            },
            using: {
               _enum: "userMaskEnabled",
               _value: "revealSelection"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "save",
            in: {
               _path: "C:\Users\Johnny\AppData\Local\Temp\Halftone Results4.psb",
               _kind: "local"
            },
            saveStage: {
               _enum: "saveStageType",
               _value: "saveBegin"
            },
            documentID: 433,
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "save",
            in: {
               _path: "C:\Users\Johnny\AppData\Local\Temp\Halftone Results4.psb",
               _kind: "local"
            },
            documentID: 433,
            saveStage: {
               _enum: "saveStageType",
               _value: "saveSucceeded"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "close",
            documentID: 433,
            _options: {
               dialogOptions: "dontDisplay"
            }
         },
         {
            _obj: "rasterizeLayer",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {
         modalBehavior: "execute"
      }
   );
}

async function runModalFunction() {
   await executeAsModal(actionCommands, {"commandName": "Action Commands"});
}

await runModalFunction();

I’ve only ever user Alchemist to get descriptors for certain actions and then embed them into helper functions. So I can’t speak to using it for creating entire scripts by chaining actions, etc. and might be missing something obvious.

That said, in my experience it’s best to operate with IDs only and not rely on layer names, unless you have a naming convention that can be enforced or guaranteed. Photoshop does not enforce unique layer names and so they are not a good identifier. IDs on the other hand are unique and stay persistent for the duration of an object’s life cycle (meaning between when a layer gets newly created and when it’s removed from a document). You can access a layer’s ID as an object property, using the API. Conversely if you have an ID you can ask Photoshop to give you the corresponding layer object of a specific doc, like so:

const photoshop = require('photoshop')
let layer = new photoshop.app.Layer(layerID, doc.id);

Note: each document has its own ID as well (which is only persistent while the document is open). The above code snipped is gonna throw an exception in case no layer of given ID exists in the document. I typically use the API to work with a document and it’s layers as objects/properties and if I need to use batchPlay for some reason I wrap that into a function with the object’s ID as argument.

Anyhow, back to your particular problem. I assume you’re allowing to select a random layer in the beginning or is it always the same layer (i.e. named a particular way)? If it’s not the same layer then obviously the layers that get hidden (i.e. the inverse of the selection) will be different with every possible selection of your target layer, so you’re batchPlay code would have to be different too for any possible incantation (or rather recompute the set of layers to hide and their identifiers).

Personally I would break this up into different (reusable) functions, rather than chaining everything. But that’s just me :grin: It might also be a bit easier to debug, as you can test each function independently.

Btw. it might be a lot easier hiding all layers and then vising the one you have selected rather than the other way around. That way you don’t need to provide names or IDs for the hiding step, you just tell Photoshop to hide everything and then later you tell Photoshop to duplicate the layer you are actually interested in and vis that one?

hello and welcome to the forums.

I encourage you to read about Functional Programming.

you basically split your code into reusable functions with clear readable names.

now you debug each function separately and make sure you get what you want out of it (expected reliable input and output).

and post whatever issues you might have with it so we are able to help you with one thing at a time

Thank you!

At the moment it’s more imperative than the strategic thinking. I think partly it’s how I’ve always learned. Dive in, hope I don’t crack my head on the pool floor and hopefully I come out the other end.

I think the organization will come with the understanding of what, where and how to work with functions as I’m all still very new to this.

How would you distill this down? What core functions would you utilize?

The intention is to take any random layer and essentially apply mask to it consisting of halftones made out of it’s greyscale values. I do a few things in-between to later present the user with an isolated (visually) result and a few small adjustments but that’s the core idea.

It’s critical to present isolate results (hiding layers and what have you) because my target audience isn’t verse in PS.

I appreciate any insights! :muscle:

Thanks for the reply!

I definitely agree with both you and Maher below, I should get to know functions better and how to utilize them more strategically. ATM it’s more spray and pray.

After doing lots of reading after posting initially, I definitely think currently, the issue I’m having (beyond the strategy) is saving smart object files, using temp storage and tokens.

The concept isn’t landing for me totally as I’m still a virgin to JS and the light turning on for me with functions their arguments, syntax and knowing which I should be looking for.

It’s just impossible to find the solution to your problem next to a breakdown. I’ve been using GPT to help me understand some basics (fully know it can’t be trusted).

Here’s a sample it out put that I feel is pretty close to a section of what I need to have happen. The section about editing the smart objects contents obviously needs my very own blend of special seasoning and the actual correct API functions but what are your thoughts on it?

const application = require("photoshop").app;
const { editDocument } = require("application");

async function createSmartObjectFromLayer() {
  // Get the active document and selected layer
  const activeDoc = application.activeDocument;
  const selectedLayer = activeDoc.layers.find(layer => layer.selected);

  // Convert the selected layer to a smart object
  const smartObject = await editDocument({
    layer: selectedLayer,
    editingMode: "solo",
    withDialogs: false,
  });

  // Get the smart object's layer ID and contents
  const smartObjectLayerID = smartObject.layerId;
  const smartObjectContents = smartObject._children;

  // Edit the smart object contents
  smartObjectContents[0].textItem.contents = "Hello, World!";

  // Get a session token for saving the smart object
  const fs = require("uxp").storage.localFileSystem;
  const smartObjectEntry = await fs.getEntryForDocument(smartObject.document);
  const sessionToken = fs.createSessionToken(smartObjectEntry);

  // Save the smart object using the session token
  await application.batchPlay([
    {
      _obj: "save",
      as: "smartObject",
      in: {
        _path: sessionToken,
        _kind: "local",
      },
      source: {
        _path: smartObjectLayerID,
        _kind: "layer",
      },
    },
  ]);
}

createSmartObjectFromLayer();

In the end, I’m just a dude with an action set that needs it into a plugin panel and eager to make that happen :sunglasses:

ok, here’s a starting point

const { app,core: { executeAsModal }} = require("photoshop");

function layersHideExcept(visibleLayersArray) {
   executeAsModal(() => {
      let activeDocument = app.activeDocument
      let docLayers = activeDocument.layers
      //loop through the layer of the active document
      for (let layer of docLayers) {
         //check if the layer is in [visibleLayersArray] array
         if (visibleLayersArray.includes(layer.name)) {
            //show layer
            layer.visible = true
         } else {
            //hide layer
            layer.visible = false
         }
      }
   })
}

this function “layersHideExcept” accepts one parameter (array of strings) this array should includes a list of the layers you want to keep visible while hiding all other layers

once defined, you can use it like so:

layersHideExcept(["Layer 1", "Layer 2", "Layer 3"])

this example will hide all layers except “Layer 1”, “Layer 2” and “Layer 3”

Thanks for jumping in with me!

This is a good start and how would I go about using @dotproducts advice of utilizing layerID’s to target the currently selected layer instead of an array of layers (which I assume an array can also contain a single argument)?

I assume something along these lines?

const activeLayers = app.activeDocuments.activeLayers;

Hopefully I’m not making you want to chuck your monitor at a wall :sweat_smile:

  • yes an array can include on element ["lonlyString"]

as a beginner I would try to stay away from batchPlay for tasks that can be done with the DOM.
for example using parts from the code I posted earlier we can manipulate layers easily.

let’s say we have a list of layers that we want to duplicate, then we hide all layers except the newly created layers… I would do it like this

notice how we break the code into functions, and we call only mainFunction() and it will use other functions as needed

async function mainFunction() {
   executeAsModal(async () => {
      //list of layers we want to duplicate
      let myLayers = ["Layer 1", "Layer 2", "Layer 3"]

      //duplicate [myLayers] and create a list of the newly created layers
      let newLayers = await duplicateLayers(myLayers)
      console.log(newLayers)
      //hide all layers except the newly created layers
      layersHideExcept(newLayers)
   })
}

async function duplicateLayers(layerToDuplicate) {
   //create an empty array
   let returnedArray = []
      let activeDocument = app.activeDocument
      let docLayers = activeDocument.layers
      //loop htrough layers
      for (let layer of docLayers) {
         //check if layer name is in the list
         if (layerToDuplicate.includes(layer.name)) {
            //duplicate layer
            let newLayer = await layer.duplicate();
            //store its name in the new array
            returnedArray.push(newLayer.name)
         }
      }
   //return the list of new names
   return returnedArray
}

function layersHideExcept(layerToKeepVisible) {
   let activeDocument = app.activeDocument
   let docLayers = activeDocument.layers
   //loop through the layer of the active document
   for (let layer of docLayers) {
      //check if the layer is in [layerToKeepVisible] array
      if (layerToKeepVisible.includes(layer.name)) {
         //show layer
         layer.visible = true
      } else {
         //hide layer
         layer.visible = false
      }
   }
}

app.activeDocuments.activeLayers inclues a list of active (selected) layers.
it includes a list of layer objects, each object hold properties and methods

an example of a property is id or name
while duplicate() is a methos (function)

so to get a list of ids of active layers we can use something like this

let activeLayersIDs = app.activeDocument.activeLayers.map(layer => layer.id)

Just a side note and humble word of caution: Don’t use GPT to do the coding for you, unless you are absolutely proficient in the corresponding language. Why? Because you won’t be able to tell whether it’s lying to you, giving you something reasonable or not. You won’t be able to properly debug the code, if it turns out it’s wrong. Another reason in this particular case is that UXP is a relatively new platform, not fully documented and GPT’s knowledge (if one may even call it that) is likely not up to date.

I understand that it’s very tempting as a newcomer to use GPT as a starting point and seemingly as shortcut, but I would strongly encourage you to do the legwork and read through some Javascript tutorials for beginners. It will be much more rewarding to be able to code yourself AND understand your code. :slight_smile:

I definitely agree and appreciate the advice.

I really use it to throw scenarios at so that I can ask it foundational questions to get answers that might be a little misleading at times but can help me point my research in the right direction. It’s pretty decent as a teacher when trying to understand certain concepts when paired with some good youtube channels.

I’ve linked it before and I’ll no doubt link it again:
I learnt JS from this course and I can’t recommend it enough.
If it’s showing it at full price add it to your cart and wait, they will send you a massive discount after a week or so. I’ve never paid more than £13 for a uDemy course.

1 Like

Timothy,

Thanks for the heads up! I’ll definitely snag that and use your tip for the discount :wink:

I do think it’s essential for the wall I’ve hit. Knowing, for what I want to accomplish there will updates and troubleshooting needed. I will need the skill and I can’t just eBeg in the forum for help every time I need something.

1 Like

I did much what you are doing but with CEP (UXP’s precursor) and built a functional plugin for my then employer. I then thought I knew JS!
That is until I tried to utilise it outside of CEP and I just kept hitting a wall of endless research with no clear answers.
I did that course and it all fell into place because I’d learnt the fundamentals of JS rather than how to reverse engineer other people’s code, and could now write my own, albeit messy code.

What you’ve clearly shown in this thread is that you’re a highly motivated self learner - trust me when I say this, if you commit to the grind of a programme of structured study you’ll be golden.
JS isn’t that complicated really, and once you get the fundamentals you’ll see that UXP, and React, Vue, Angular, or what ever framework, library, or module you use is all basically just an opinionated way of writing the same JavaScript .

That’s the rub of trying to jump straight into UXP, you’re trying to learn JS whilst also having to deal with notoriously hard to grasp concepts like asynchronous functions and fucking proxies , and on top of that Adobe’s opinions on how that should be done.
It’s like trying to learn a foreign language by only reading legal documents or finance agreements :rofl:

5 Likes

I learned the basics from random videos here and there, after that I dove into a series called coding rainbow (now coding train).

I fell in love with coding, so many sleepless nights and lost hair over very basic issues. but that’s how you learn.

once you break the ice with your choice of tutorial source. reading through MDN will do the rest.

here in the forums, people are friendly and happy to help just try your best, read the docs and come up with a starting code

old topics are a treasure, read through them, review the code and adjust to your need.

3 Likes

I pour one out for the stack overflow users that showed heroic patience explaining basic shit to me :pouring_liquid:

I think I can safely speak for all the regular members of this forum, be they OG coders or hobbyists with knowledge, we’ll always be more responsive to a poster that demonstrates having made an attempt to solve it themselves than someone who just bluntly asks for code.
This is by far the most supportive niche coding forum I’m a member of!

3 Likes

Thanks for sharing your journey, it can be a big help in setting small goals for myself whilst knowing the struggle is real, for everyone.

I always appreciate the takes on how things get compartmentalized in someone’s learning process because there are awesome nuggets of gold if you read between the lines hard enough.

I’m definitely motivated as I’m an artist-illustrator/ “Photoshop” guy and have lots of create ideas and have come to the realization that there isn’t any shortcuts. I just need a few good examples to understand why certain decisions are being made in the code to better grasp the landscape to then seek and find the right research.

Believe me, if I want someone to write the code for me, I’d happily pay but I’m one of those “Take the VCR apart to fix it” guys so that I know how to do it later.

The concepts are definitely hard for me being such an intensely visual learner. I honestly spend more time finding the best analogies to get the light switch to flip on, than I care to admit :sweat_smile:

I’ll definitely look into the Coding Train as well so thanks for the tip :wink:

I dig coding as well and learned it autodidactically in the medieval times before YouTube lol.

I appreciate the initial code as it has blown my hair back enough to be my first project to investigate and understand. The hard part for me is going to be find what syntax comes from where.

In my mind there’s foundations of Java, Photoshop API, UXP API and then custom stuff.

:: INSERT TIM & ERIC MIND BLOWN GIF HERE::

If I could play Devil’s advocate for a moment, at risk of getting the finger wag.

bP is essentially the layman-coder’s version of hacking “Actions” into an extension. (Although understanding fully, it should truly be used as an inspector more than a copy-paste code provider, although the videos people make using it would lead you to think otherwise)

Is performance the only issue with going that route? (Beyond future API updates wreaking havoc for support later on)

I ask because I juggle a couple scenarios in my mind.

Handing my current customers a “working” (all be it, copy pasta version) extension for an existing product of action-sets, while learning JS to better support the product itself, later on. (Mainly because the money can fund the time needed to learn) and create new one’s with the newly acquired skill set.

Or just waiting until I can handle JS and coding it properly to put it’s best foot forward?

Keeping in mind, I do have a small window in which I can take advantage of the opportunity before the void is filled by another.

Photoshop’s UXP is a structure of ancient sorcery (ActionDescriptor) wrapped in modern JavaScript.

The batchPlay, which handles the ActionDescriptor directly, is closer to the essence than the API. If you write the best code in the world, batchPlay should perform as well as or better than the API.

Usually, an API written by an Adobe member is faster, better quality, more stable, and expected to be maintained better than our own code. It may be a good idea to use the API.

However, if you already have code that works correctly, I recommend publishing it. Mark Zuckerberg also says “Done is better than perfect”.