Duplicate a layer without bP

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