Duplicate a layer without bP

Would someone be willing to walk me through step by step of how to accomplish duplicating the active layer in a doc without replying on bP,? How to tie it into an eventlistener button and maybe get it to work?

I’m trying to wrap my head around the fundamentals of something seemingly simple.

I was hoping it would be more of a conversation explaining the steps to gain understanding on why lines are chosen or asking me why I made decisions (to laugh at me, sure or help push me in the right direction :stuck_out_tongue:)

After my first post (which was incredibly enlightening and serious humble pie, thanks @Timothy_Bennett & @Maher ++) I’ve been hitting the tuts and reading hard after so many fantastic suggestions but have hit a plateau in my frustration levels and was hoping to find vent for it with a small win…

What I’ve tried with:

const app = window.require("photoshop").app;

const doc = app.activeDocument;

require('photoshop').core.executeAsModal(duplicateActiveLayer, { "commandName": "Duplicate Layer" });

async function duplicateActiveLayer() {

const copyLayer = await layer.duplicate()

}

button:
document.getElementById("btnSimpleClean").addEventListener("click", simpleClean);

My biggest frustration at the moment is understanding how to assemble executemodals and async properly and reference the functions that need them. I’m like a boat adrift.

I hate being so reliant on visual learning but I am. Would love to distill any ideas into graphics (it’s like doing my spelling words three times each) when I have the time, might help others.

If you bare with me I’ll knock up a demo plugin for you today.

Here we go, hopefully the comments are self-explanatory, if not, ask away!

// Import modules
const { app, core } = require("photoshop");

// Async function - await can be used within
const duplicateActiveLayers = async () => {
  // Wrapped in try/catch to report errors
  try {
    // Get active document
    const doc = app.activeDocument;
    // Get selected Layers - returns an Array
    const activeLayers = doc.activeLayers;

    // Loop selected layers - using for...of loop as it allows async within, unlike forEach
    for (const layer of activeLayers) {
      // Using executeAsModal as duplication changes PS state.
      // executeAsModal receives a callback function as its first argument, this is an important distinction - a function, not the result of a function!
      // In this instance an anonymous arrow function is used, and the optional second argument is omitted.
      await core.executeAsModal(async () => {
        await layer.duplicate();
      });
    }
  } catch (error) {
    console.log("duplicateActiveLayer error:", error);
  }
};

// Event Listener - button click executes duplicateActiveLayers function.
// Although duplicateActiveLayers() is an async function we can't use await for the event listener callback,
// but this is fine as it runs in isolation:  all the code is encapsulated within the function and there is no code following it that relies on the result.
document
  .querySelector("#btnDupe")
  .addEventListener("click", duplicateActiveLayers);
1 Like

The only mistakes with your code were:

// removed window from import
const app = require("photoshop").app; 

const doc = app.activeDocument;

// added await as executeAsModal is async
await require('photoshop').core.executeAsModal(duplicateActiveLayer, { "commandName": "Duplicate Layer" });

async function duplicateActiveLayer() {

// layer is not defined 
const copyLayer = await layer.duplicate()

What I did differently was:
• actually define layer! :rofl:
• import app and core via the same import statement at the top of my code.
• declared doc within my function rather than globally - the user might have changed document since plugin load
• put executeAsModal inside my function - essentially leaving it until absolutely necessary and only encapsulating code that needs it. Your version basically does the same thing but the other way around.
• wrap function logic in try/catch for error reporting

1 Like

@Timothy_Bennett First off and most importantly, you’re awesome. Seriously, I come across a lot of your replies in my hunts and that just means your incredibly helpful and I’m grateful for that!

So this is a great start for me and has already helped immensely. It’s helping me further to understand how to properly read the documentation! :clap:

If you don’t mind, I’ll trying to put my thoughts and questions together. I don’t want to be all over the place and as precise as I can be to make sure I don’t waste any more of your time.

I’m drawing everything out and trying to understand how to structure the next couple steps to eventually complete an action (set of steps) I plan on doing.

1 Like

Here’s a little diagram I illustrated: Adobe Photoshop

I’m trying to really understand the planning of the functionality and obviously have lots to learn but going block by block and stepping back from time to time, to see the whole picture is what really helps me.

So the next step I want to do is add a mask that is derived from the menu “Layer/Layer Mask/From Transparency” and apply it to the newly create duplicate layer. but looked and couldn’t find it in UXP so naturally we head to bP. (I assume)

So I think in the next bit of code I would have to:

  1. Target the duplicate layer
  2. bP the mask to it (new duplicate layer created prior), using the method described above.

This is where I get tripped up a lot. Trying to understand how to properly run a bP. I look a the Alchemist code and understand what I’m looking at to some degree but never works when trying it.

Example:

//Modules and methods being declared
const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;

//wrapping batchPlay in an async function
async function actionCommands() {
   const result = await batchPlay(
      [
         {
            _obj: "make",
            new: { 
               _class: "channel" 
            },
            at: {
               _ref: "channel",
               _enum: "channel",
               _value: "mask"
            },
            using: {
               _enum: "userMaskEnabled",
               _value: "transparency"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {
         synchronousExecution: false
      }
   );
}

//function declared to run actionCommands wrapped in async func because of executeAsModal
async function runModalFunction() {
   await executeAsModal(actionCommands, {"commandName": "Action Commands"});
}

await runModalFunction(); //confusing because I assume this has to be wrapped in an async function?

This is where I’m find lots of trouble understanding bP as well.

  • I look and see methods and modules being called up-top (core, bP)
  • I see the first async function “actionCommands”, I believe that is generic and provided by Alchemist?
  • The action descriptors themselves, I’m generally lost, especially in this specific case. The “new:, at:, using:” is an array of the keyword “_obj” I would assume but no clue where to find them in the documentation.
  • still not clear when to envoke true or false synchronousExecution or the types of modalBehavior
    Then

Stepping back from bP, I’m trying to understand the proper way to coordinate UXP and bP interplay on top if these issues (bullet list above).

Hopefully is isn’t as confusing to understand :sweat_smile:

Glad to have been helpful!

I’ll do my best to address all your points, but I’ll focus on structure/functional programming.

I’m going to demonstrate a simple 3-stage process using the API and batchPlay and then I’ll show how to make it more streamlined and D.R.Y. (“Don’t Repeat Yourself”) and talk about why this is better as I go.

Here’s the first version - the process is:

  1. make filled triangle path as new layer
  2. reduce opacity by a value
  3. flatten document

The first two stages are completed via batchPlay and the flatten is done via the API.
The event listener triggers what I’m calling a “core process” function - this is a larger function that encapsulates an entire workflow and calls a series of “helper” functions (one for each step of the process).

// Imports
const {app, core} = require("photoshop");
const {batchPlay} = require("photoshop").action;

// Helper Functions
const makeTriangle = async () => {
  // Please excuse the length of this function, you can essentially ignore the batchPlay 
  // descriptor's details, they're not relevant to the example.
  return await core.executeAsModal(async () => {
      return await batchPlay(
        [
         {
            _obj: "make",
            _target: [
               {
                  _ref: "contentLayer"
               }
            ],
            using: {
               _obj: "contentLayer",
               type: {
                  _obj: "solidColorLayer",
                  color: {
                     _obj: "RGBColor",
                     red: 0.0038910505827516317,
                     grain: 255,
                     blue: 11.996109243482351
                  }
               },
               shape: {
                  _obj: "triangle",
                  keyOriginType: 7,
                  keyOriginBoxCorners: {
                     rectangleCornerA: {
                        _obj: "paint",
                        horizontal: 492,
                        vertical: 285
                     },
                     rectangleCornerB: {
                        _obj: "paint",
                        horizontal: 1054,
                        vertical: 285
                     },
                     rectangleCornerC: {
                        _obj: "paint",
                        horizontal: 1054,
                        vertical: 1069
                     },
                     rectangleCornerD: {
                        _obj: "paint",
                        horizontal: 492,
                        vertical: 1069
                     }
                  },
                  keyOriginPolySides: 3,
                  keyOriginShapeBBox: {
                     _obj: "classFloatRect",
                     top: 285,
                     left: 492,
                     bottom: 1069,
                     right: 1054
                  },
                  keyOriginPolyPreviousTightBoxCorners: {
                     rectangleCornerA: {
                        _obj: "paint",
                        horizontal: 492,
                        vertical: 285
                     },
                     rectangleCornerB: {
                        _obj: "paint",
                        horizontal: 1054,
                        vertical: 285
                     },
                     rectangleCornerC: {
                        _obj: "paint",
                        horizontal: 1054,
                        vertical: 1069
                     },
                     rectangleCornerD: {
                        _obj: "paint",
                        horizontal: 492,
                        vertical: 1069
                     }
                  },
                  keyOriginPolyTrueRectCorners: {
                     rectangleCornerA: {
                        _obj: "paint",
                        horizontal: 492,
                        vertical: 285
                     },
                     rectangleCornerB: {
                        _obj: "paint",
                        horizontal: 1054,
                        vertical: 285
                     },
                     rectangleCornerC: {
                        _obj: "paint",
                        horizontal: 1054,
                        vertical: 1069
                     },
                     rectangleCornerD: {
                        _obj: "paint",
                        horizontal: 492,
                        vertical: 1069
                     }
                  },
                  keyOriginPolyPixelHSF: 1,
                  transform: {
                     _obj: "transform",
                     xx: 1,
                     xy: 0,
                     yx: 0,
                     yy: 1,
                     tx: 0,
                     ty: 0
                  },
                  sides: 3,
                  polygonCornerRadius: {
                     _unit: "distanceUnit",
                     _value: 0
                  }
               },
               strokeStyle: {
                  _obj: "strokeStyle",
                  strokeStyleVersion: 2,
                  strokeEnabled: true,
                  fillEnabled: true,
                  strokeStyleLineWidth: {
                     _unit: "pixelsUnit",
                     _value: 1
                  },
                  strokeStyleLineDashOffset: {
                     _unit: "pointsUnit",
                     _value: 0
                  },
                  strokeStyleMiterLimit: 100,
                  strokeStyleLineCapType: {
                     _enum: "strokeStyleLineCapType",
                     _value: "strokeStyleButtCap"
                  },
                  strokeStyleLineJoinType: {
                     _enum: "strokeStyleLineJoinType",
                     _value: "strokeStyleMiterJoin"
                  },
                  strokeStyleLineAlignment: {
                     _enum: "strokeStyleLineAlignment",
                     _value: "strokeStyleAlignCenter"
                  },
                  strokeStyleScaleLock: false,
                  strokeStyleStrokeAdjust: false,
                  strokeStyleLineDashSet: [],
                  strokeStyleBlendMode: {
                     _enum: "blendMode",
                     _value: "normal"
                  },
                  strokeStyleOpacity: {
                     _unit: "percentUnit",
                     _value: 100
                  },
                  strokeStyleContent: {
                     _obj: "solidColorLayer",
                     color: {
                        _obj: "RGBColor",
                        red: 0,
                        grain: 0,
                        blue: 0
                     }
                  },
                  strokeStyleResolution: 300
               }
            },
            layerID: 6,
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
    )
  })
}

const reduceOpacity = async (percent) => {
  try {
   return await core.executeAsModal(async () => {
      return await batchPlay(
        [
          {
            _obj: "set",
            _target: [
              {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
              }
            ],
            to: {
              _obj: "layer",
              opacity: {
                  _unit: "percentUnit",
                  _value: percent
              }
            },
            _options: {
              dialogOptions: "dontDisplay"
            }
          }
        ],
      {}
    )
  });
       
  } catch (error) {
    console.log(error)
  }
}

const flatten = async (doc) => {
  await core.executeAsModal(async () => { 
    return await doc.flatten()
    })
}

// Core Processes
const mainProcess = async () => {
  try {
    const doc = app.activeDocument;

    await makeTriangle();
    await reduceOpacity(50)
    await flatten(doc)

  } catch (error) {
    console.log(error)
  }
}

// Event Listeners
document.getElementById("btnPopulate").addEventListener("click", mainProcess);

The basic structure of this JS file is like this:

// Import Statements
// Define Global Functions
// Define Global Variables - none defined in this example, but including for completeness
// Event Listeners

and you can visualise it something like this:

Things to note:

  • makeTriangle() has all its values hard-coded into the batchPlay descriptor, whereas reduceOpacity() take a percentage value as an argument and bops that into the batchPlay descriptor.
  • All three helper functions call executeAsModal and two call batchPlay separately.

There’s a bunch of repeated code so let’s make it better!

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

const batchPLayRunner = async (commands) => {
  const {batchPlay} = require("photoshop").action;
  try {
    return await core.executeAsModal(async () => {
      return await batchPlay(
        [...commands], {}
      )
    })
  } catch (error) {
    console.log(error)
  }
}

const makeTriangle = async () => {
  const descriptors = [
    {
      _obj: "make",
      _target: [
          {
            _ref: "contentLayer"
          }
      ],
      using: {
          _obj: "contentLayer",
          type: {
            _obj: "solidColorLayer",
            color: {
                _obj: "RGBColor",
                red: 0.0038910505827516317,
                grain: 255,
                blue: 11.996109243482351
            }
          },
          shape: {
            _obj: "triangle",
            keyOriginType: 7,
            keyOriginBoxCorners: {
                rectangleCornerA: {
                  _obj: "paint",
                  horizontal: 492,
                  vertical: 285
                },
                rectangleCornerB: {
                  _obj: "paint",
                  horizontal: 1054,
                  vertical: 285
                },
                rectangleCornerC: {
                  _obj: "paint",
                  horizontal: 1054,
                  vertical: 1069
                },
                rectangleCornerD: {
                  _obj: "paint",
                  horizontal: 492,
                  vertical: 1069
                }
            },
            keyOriginPolySides: 3,
            keyOriginShapeBBox: {
                _obj: "classFloatRect",
                top: 285,
                left: 492,
                bottom: 1069,
                right: 1054
            },
            keyOriginPolyPreviousTightBoxCorners: {
                rectangleCornerA: {
                  _obj: "paint",
                  horizontal: 492,
                  vertical: 285
                },
                rectangleCornerB: {
                  _obj: "paint",
                  horizontal: 1054,
                  vertical: 285
                },
                rectangleCornerC: {
                  _obj: "paint",
                  horizontal: 1054,
                  vertical: 1069
                },
                rectangleCornerD: {
                  _obj: "paint",
                  horizontal: 492,
                  vertical: 1069
                }
            },
            keyOriginPolyTrueRectCorners: {
                rectangleCornerA: {
                  _obj: "paint",
                  horizontal: 492,
                  vertical: 285
                },
                rectangleCornerB: {
                  _obj: "paint",
                  horizontal: 1054,
                  vertical: 285
                },
                rectangleCornerC: {
                  _obj: "paint",
                  horizontal: 1054,
                  vertical: 1069
                },
                rectangleCornerD: {
                  _obj: "paint",
                  horizontal: 492,
                  vertical: 1069
                }
            },
            keyOriginPolyPixelHSF: 1,
            transform: {
                _obj: "transform",
                xx: 1,
                xy: 0,
                yx: 0,
                yy: 1,
                tx: 0,
                ty: 0
            },
            sides: 3,
            polygonCornerRadius: {
                _unit: "distanceUnit",
                _value: 0
            }
          },
          strokeStyle: {
            _obj: "strokeStyle",
            strokeStyleVersion: 2,
            strokeEnabled: true,
            fillEnabled: true,
            strokeStyleLineWidth: {
                _unit: "pixelsUnit",
                _value: 1
            },
            strokeStyleLineDashOffset: {
                _unit: "pointsUnit",
                _value: 0
            },
            strokeStyleMiterLimit: 100,
            strokeStyleLineCapType: {
                _enum: "strokeStyleLineCapType",
                _value: "strokeStyleButtCap"
            },
            strokeStyleLineJoinType: {
                _enum: "strokeStyleLineJoinType",
                _value: "strokeStyleMiterJoin"
            },
            strokeStyleLineAlignment: {
                _enum: "strokeStyleLineAlignment",
                _value: "strokeStyleAlignCenter"
            },
            strokeStyleScaleLock: false,
            strokeStyleStrokeAdjust: false,
            strokeStyleLineDashSet: [],
            strokeStyleBlendMode: {
                _enum: "blendMode",
                _value: "normal"
            },
            strokeStyleOpacity: {
                _unit: "percentUnit",
                _value: 100
            },
            strokeStyleContent: {
                _obj: "solidColorLayer",
                color: {
                  _obj: "RGBColor",
                  red: 0,
                  grain: 0,
                  blue: 0
                }
            },
            strokeStyleResolution: 300
          }
      },
      layerID: 6,
      _options: {
          dialogOptions: "dontDisplay"
      }
    }
  ];

  await batchPLayRunner(descriptors)
}

const reduceOpacity = async (percent) => {
  try {
    const descriptors =
        [
          {
            _obj: "set",
            _target: [
              {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
              }
            ],
            to: {
              _obj: "layer",
              opacity: {
                  _unit: "percentUnit",
                  _value: percent
              }
            },
            _options: {
              dialogOptions: "dontDisplay"
            }
          }
        ];

        await batchPLayRunner(descriptors)
  } catch (error) {
    console.log(error)
  }
}

const flatten = async (doc) => {
  await core.executeAsModal(async () => { 
    return await doc.flatten()
    })
}

const mainProcess = async () => {
  try {
    const doc = app.activeDocument;

    await makeTriangle();
    await reduceOpacity(50)

    await flatten(doc);
  } catch (error) {
    console.log(error)
  }
}

document.getElementById("btnPopulate").addEventListener("click", mainProcess);

The big difference is the addition of the batchPlayRunner function - this takes an array of batchPlay descriptors and, well, runs them. This means we don’t have to write out all that awkward executeAsModal/batchPlay code out again and again. Now all we do in each helper function is construct an array of descriptors and pass it to batchPlayRunner(). We now also know that if it errors it’s not down to how the batchPlay is being run, but rather the descriptor itself or the state of Photoshop. Note that the batchPlay import has been moved into the runner function as it’s not needed anywhere else in the code.

Whilst this is nice and neat and allows for easy reading and editing it would quickly become a very cumbersome file. Ideally one would move all the functions into a module (a separate JS file that exports them) or two and import the core process only.

To answer your questions:

I hope all this makes sense!

1 Like

Thanks again @Timothy_Bennett it’s really helping understand the landscape for functional programming and getting bP to roll around with PS api, to do so.

Here’s what I gather and hopefully have organized visually in a different way then yours (This is the refined code after the first example of you previous post):

Now here’s me putting your knowledge to work and immediately falling on my face :upside_down_face::

// Creating an async parent function to allow for await reliant methods/etc.
const groupSelectedLayers = async () => {

  try {
// Import (local scope of "groupSelectedLayers" function) active document to allow use of it's prop's & meth's
// Import (local scope of "groupSelectedLayers" function) active document to allow use of it's prop's & meth's
    const doc = app.activeDocument;
// Import and creating a function for an active laver/s
    const activeLayers = doc.activeLayers;

    for (const layer of activeLayers) {
      await core.executeAsModal(async () => {
// Where I think I'm grouping the actively selected layer/s but falling on my face
        const nonEmptyGroup = await doc.createLayerGroup({ name: "group", fromLayers: [activeLayers] })
      });
    }
  } catch (error) {
    console.log("groupSelectedLayers error:", error);
  }
};
document
  .querySelector(".btngroupLayers")
  .addEventListener("click", groupSelectedLayers);

So this obviously doesn’t work and I think mostly due to looking at the documentation, understanding parts but not as a whole.

I find myself being able to read and understand but tying them together seems foggy and vague. Especially when finding the task I need to complete. Finding the class and it’s methods to accomplish something like putting the currently active single layer, into a group, all while trying to navigate properly and apply executeAsModal, async and the like, when needed.

Would it be a huge ask to walk through the documentation on assembling the task of putting a single, actively selected layer, into a group without bP? (basically like having a selected layer and hitting ctrl + g)

Also, can I treat you to a coffee and a bite, if you have a place I can send a little $. I would love to because you’ve been so generous!

You are a lot closer than you think! :muscle:
createLayerGroup() takes an array as the value for the fromLayers option, which is what you are giving it, but the array is not formed as the method is expecting…
You correctly pass an array:
fromLayers: []
but activeLayers is already an array , so when you pass it like this:
fromLayers: [activeLayers]
what you are actually doing is making the activeLayers array the first element in another array.
fromLayers is expecting an array like this:

[ // activeLayers array
  { //... A layer object },
  { //... Another layer object if multiple layers selected },
]

but you are passing this:

[ // Outer array
  [ // Inner array - activeLayers
    { //... A layer object },
    { //... Another layer object if multiple layers selected },
  ]
]

You can resolve this in one of two ways:

  • fromLayers: activeLayers - just pass the array as it is
  • fromLayers: [...activeLayers] - a little more esoteric, and overkill for this, but I’m including for completeness. This is using the spread operator

I’ll note that if the selected layer is locked (like the background layer) then it’ll create the folder but the layer won’t be moved and it won’t error.

@Timothy_Bennett Glad to hear I was close! Still feel like I’m on shaky ground (I’m sure I’m a long way away from having my sea legs lol) :sweat_smile:

Here’s the tweak and unfortunately nothing happens and debug doesn’t seem to find anything but the event lister null properties not being read.


const groupSelectedLayers = async () => {

  try {
    const doc = app.activeDocument;
    const activeLayers = doc.activeLayers;

    for (const layer of activeLayers) {
      await core.executeAsModal(async () => {
        const nonEmptyGroup = await doc.createLayerGroup({ name: "group", fromLayers: activeLayers })
      });
    }
  } catch (error) {
    console.log("groupSelectedLayers error:", error);
  }
};

document
  .querySelector(".btnGroupLayers")
  .addEventListener("click", groupSelectedLayers);

Not sure why this doesn’t work, any insights? I did try both versions you suggested including the spread operator.

I feel like I might be over looking something and I did check to make sure the html paired with the button (copy pasta).

Hmm, it does work, so there must be something else wrong in your code. My first guess would be that you’re missing an import or one is malformed.
Can you post your whole JS file please?

If you’re feeling inclined to debug it yourself - console.log everything:

const groupSelectedLayers = async () => {
  console.log("groupSelectedLayers() triggered")
  try {
    const doc = app.activeDocument;
    const activeLayers = doc.activeLayers;

    console.log("variables", doc, activeLayers)

    for (const layer of activeLayers) {

      console.log("looping", layer)

      await core.executeAsModal(async () => {

       console.log("executingAsModal")

        const nonEmptyGroup = await doc.createLayerGroup({ name: "group", fromLayers: activeLayers });

        console.log("result", nonEmptyGroup)
      });
    }
  } catch (error) {
    console.log("groupSelectedLayers error:", error);
  }
};

document
  .querySelector(".btnGroupLayers")
  .addEventListener("click", groupSelectedLayers);