Copy link to clipboard
Copied
Hi all,
I have quite an intermediate understanding of Photoshop, but can't figure out an action or method for quickly creating duplicates of a product within a canvas to then be uploaded to online marketplaces, for example, I have the potential to sell products in multi-packs, there are quite a few sellers online that sell this way and they also create duplicate images of the product for these multi-pack variation ads.
Please see below for an example of multi-pack images that are used for the main image of the advertisements online.
As you can see, all of these seem to be created in photoshop using some form of action to quickly do this in bulk - does anyone have specific knowledge as to how to recreate this process quickly?
Many thanks.
*EDIT – 8th April 2022: I have updated the code to a 1.4 version. Version 1.3 was updated to work with vector layer content. Version 1.2 includes the optional ability to call the Fit Image script or the Image Size command. Simply remove the // double slash comments. If enabled, these options will bring up a second dialog after script completion.
@JJMack - thank you for the feedback.
No, it was not designed to resize for web, all it does is create N amount of horizontal and vertical copies
...
The following “Multi Pack Generator" script offers the following features:
Copy link to clipboard
Copied
Hi, do you know how to work with smart objects? maybe that is part of the solution
also you can work with data variable in Photoshop, check this link
https://helpx.adobe.com/photoshop/using/creating-data-driven-graphics.html
My Best
E
Copy link to clipboard
Copied
Edited to add: Yes, an action can automate this task, but you would need different actions or running the same action multiple times, depending on how the action is built.
Scripting provides more flexibility and power.
For starters:
https://morris-photographics.com/photoshop/scripts/array-generator.html
Copy link to clipboard
Copied
It appears that the script from Trevor Morris linked above does not work correctly with current versions of Photoshop.
Rather than debugging the code, I decided to create my own:
/*
Step & Repeat N Times.jsx
v1.0 - Stephen Marsh, 2nd January 2022
https://community.adobe.com/t5/photoshop-ecosystem-discussions/creating-duplicate-images-for-multi-packs-on-online-marketplaces-within-photoshop/td-p/12511723
*/
#target photoshop
/* ScriptUI Dialog */
// Dialog
var dialog = new Window("dialog");
dialog.text = "Step & Repeat N Times";
dialog.preferredSize.width = 280;
dialog.preferredSize.height = 135;
dialog.orientation = "row";
dialog.alignChildren = ["center", "top"];
dialog.spacing = 10;
dialog.margins = 10;
// Copies group
var copiesGroup = dialog.add("group", undefined, {
name: "copiesGroup"
});
copiesGroup.orientation = "column";
copiesGroup.alignChildren = ["left", "top"];
copiesGroup.spacing = 5;
copiesGroup.margins = 0;
var labelCopiesX = copiesGroup.add("statictext", undefined, undefined, {
name: "labelCopiesX"
});
labelCopiesX.text = "X Copies:";
// Note: use editnumber instead of edittext for numerical fields!
var copiesX = copiesGroup.add('editnumber {properties: {name: "copiesX"}}');
copiesX.text = "1";
copiesX.preferredSize.width = 50;
copiesX.alignment = ["fill", "top"];
// Preset the first field to be selected/active
copiesX.active = true;
var labelCopiesY = copiesGroup.add("statictext", undefined, undefined, {
name: "labelCopiesY"
});
labelCopiesY.text = "Y Copies:";
var copiesY = copiesGroup.add('editnumber {properties: {name: "copiesY"}}');
copiesY.text = "1";
copiesY.preferredSize.width = 50;
copiesY.alignment = ["fill", "top"];
// Info footer text
/*
var infoGroup = copiesGroup.add("group", undefined, {name: "infoGroup"});
infoGroup.orientation = "column";
infoGroup.alignChildren = ["left","top"];
infoGroup.spacing = 10;
infoGroup.margins = [0,10,0,0];
infoGroup.alignment = ["left","top"];
var infoText = infoGroup.add("statictext", undefined, undefined, {name: "infoText"});
infoText.text = "v1.0 - Stephen Marsh, 2nd January 2022";
*/
// Gap group
var gapGroup = dialog.add("group", undefined, {
name: "gapGroup"
});
gapGroup.orientation = "column";
gapGroup.alignChildren = ["left", "top"];
gapGroup.spacing = 5;
gapGroup.margins = 0;
var labelGapX = gapGroup.add("statictext", undefined, undefined, {
name: "labelGapX"
});
labelGapX.text = "X Gap (px):";
var gapX = gapGroup.add('editnumber {properties: {name: "gapX"}}');
gapX.text = "0";
gapX.preferredSize.width = 50;
gapX.alignment = ["fill", "top"];
var labelGapY = gapGroup.add("statictext", undefined, undefined, {
name: "labelGapY"
});
labelGapY.text = "Y Gap (px):";
var gapY = gapGroup.add('editnumber {properties: {name: "gapY"}}');
gapY.text = "0";
gapY.preferredSize.width = 50;
gapY.alignment = ["fill", "top"];
// Border group
var borderGroup = dialog.add("group", undefined, {
name: "borderGroup"
});
borderGroup.orientation = "column";
borderGroup.alignChildren = ["left", "top"];
borderGroup.spacing = 5;
borderGroup.margins = 0;
var labelBorder = borderGroup.add("statictext", undefined, undefined, {
name: "labelBorder"
});
labelBorder.text = "Border (px):";
var border = borderGroup.add('editnumber {properties: {name: "border"}}');
border.text = "0";
border.preferredSize.width = 50;
border.alignment = ["fill", "top"];
var okButton = borderGroup.add("button", undefined, undefined, {
name: "okButton"
});
okButton.text = "OK";
okButton.alignment = ["fill", "top"];
var cancelButton = borderGroup.add("button", undefined, undefined, {
name: "cancelButton"
});
cancelButton.text = "Cancel";
cancelButton.alignment = ["fill", "top"];
// Remember, digits entered into fields are strings, not numbers!
/* Main script */
function main() {
// Ensure that the active layer is not a Background, is only a single layer and that the content is not empty
if (!app.activeDocument.activeLayer.isBackgroundLayer && app.activeDocument.layers.length === 1) {
// Render the GUI and OK button logic
if (dialog.show() === 1) {
// Call the function
arrayGenerator();
// End of script notification
app.beep();
}
} else {
alert("This script is designed to work only on a non-Background, single layer document!");
}
}
app.activeDocument.suspendHistory("Step & Repeat N Times", "main()");
/* Main script function */
function arrayGenerator() {
var doc = app.activeDocument;
// Convert GUI variable strings to numbers...
// var copiesX2 = Math.floor(copiesX.text);
// The “double tilde” (~~) operator is a double NOT Bitwise operator. Use it as a substitute for Math.floor(), since it’s faster.
var copiesX2 = ~~copiesX.text;
var copiesY2 = ~~copiesY.text;
var gapX2 = ~~gapX.text;
var gapY2 = ~~gapY.text;
var border2 = ~~border.text;
/*
alert(copiesX2.toSource())
alert(copiesY2.toSource())
alert(gapX2.toSource())
alert(gapY2.toSource())
alert(border2.toSource())
*/
// Convert to % for relative canvas resize
var newCanvasX = copiesX2 * 100;
var newCanvasY = copiesY2 * 100;
var savedRuler = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// Relative % canvas resize
relativeCanvasSizePercent(true, newCanvasX, newCanvasY);
// Absolute px canvas resize
doc.resizeCanvas(doc.width + gapX2 * copiesX2, doc.height + gapY2 * copiesY2, AnchorPosition.TOPLEFT);
// Dupe X loop
for (var i = 0; i < copiesX2; i++) {
doc.activeLayer.duplicate();
}
// Align X to right
align2SelectAll('AdRg');
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
if (copiesX2 > 1) {
// Distribute X: for 3 or more layers
var iddistort = stringIDToTypeID("distort");
var desc788 = new ActionDescriptor();
var idnull = stringIDToTypeID("null");
var ref298 = new ActionReference();
var idlayer = stringIDToTypeID("layer");
var idordinal = stringIDToTypeID("ordinal");
var idtargetEnum = stringIDToTypeID("targetEnum");
ref298.putEnumerated(idlayer, idordinal, idtargetEnum);
desc788.putReference(idnull, ref298);
var idusing = stringIDToTypeID("using");
var idalignDistributeSelector = stringIDToTypeID("alignDistributeSelector");
var idADSDistH = stringIDToTypeID("ADSDistH");
desc788.putEnumerated(idusing, idalignDistributeSelector, idADSDistH);
executeAction(iddistort, desc788, DialogModes.NO);
}
// Merge visible layers
if (doc.layers.length > 1) {
var idmergeVisible = stringIDToTypeID("mergeVisible");
executeAction(idmergeVisible, undefined, DialogModes.NO);
}
// Rename layer
doc.activeLayer.name = "Step & Repeat";
// Dupe Y loop
for (var i = 0; i < copiesY2; i++) {
doc.activeLayer.duplicate();
}
// Align Y to bottom
align2SelectAll('AdBt');
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
if (copiesY2 > 1) {
// Distribute Y: for 3 or more layers
var iddistort = stringIDToTypeID("distort");
var desc2102 = new ActionDescriptor();
var idnull = stringIDToTypeID("null");
var ref945 = new ActionReference();
var idlayer = stringIDToTypeID("layer");
var idordinal = stringIDToTypeID("ordinal");
var idtargetEnum = stringIDToTypeID("targetEnum");
ref945.putEnumerated(idlayer, idordinal, idtargetEnum);
desc2102.putReference(idnull, ref945);
var idusing = stringIDToTypeID("using");
var idalignDistributeSelector = stringIDToTypeID("alignDistributeSelector");
var idADSDistV = stringIDToTypeID("ADSDistV");
desc2102.putEnumerated(idusing, idalignDistributeSelector, idADSDistV);
executeAction(iddistort, desc2102, DialogModes.NO);
}
// Merge visible layers
if (doc.layers.length > 1) {
var idmergeVisible = stringIDToTypeID("mergeVisible");
executeAction(idmergeVisible, undefined, DialogModes.NO);
}
// Rename layer
doc.activeLayer.name = "Step & Repeat";
// Optional outer margin absolute px canvas resize
doc.resizeCanvas(doc.width + border2 * 2, doc.height + border2 * 2, AnchorPosition.MIDDLECENTER);
app.preferences.rulerUnits = savedRuler;
}
/* Helper functions for main script */
function relativeCanvasSizePercent(relative, width, height) {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
descriptor.putBoolean(s2t("relative"), relative);
descriptor.putUnitDouble(s2t("width"), s2t("percentUnit"), width);
descriptor.putUnitDouble(s2t("height"), s2t("percentUnit"), height);
descriptor.putEnumerated(s2t("horizontal"), s2t("horizontalLocation"), s2t("left"));
descriptor.putEnumerated(s2t("vertical"), s2t("verticalLocation"), s2t("top"));
executeAction(s2t("canvasSize"), descriptor, DialogModes.NO);
}
function align2SelectAll(method) {
/*
AdLf = Align Left
AdRg = Align Right
AdCH = Align Centre Horizontal
AdTp = Align Top
AdBt = Align Bottom
AdCV = Align Centre Vertical
*/
app.activeDocument.selection.selectAll();
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
desc.putReference(charIDToTypeID("null"), ref);
desc.putEnumerated(charIDToTypeID("Usng"), charIDToTypeID("ADSt"), charIDToTypeID(method));
try {
executeAction(charIDToTypeID("Algn"), desc, DialogModes.NO);
} catch (e) {}
app.activeDocument.selection.deselect();
}
https://prepression.blogspot.com/2017/11/downloading-and-installing-adobe-scripts.html
Copy link to clipboard
Copied
Your script works but its UI is not intuitive. To get a 12 Pack a 4 x 3 layout I need to enter 3 x 2 and the layout is not scaled for the web. It is 4000 px by 4500 px.
Copy link to clipboard
Copied
*EDIT – 8th April 2022: I have updated the code to a 1.4 version. Version 1.3 was updated to work with vector layer content. Version 1.2 includes the optional ability to call the Fit Image script or the Image Size command. Simply remove the // double slash comments. If enabled, these options will bring up a second dialog after script completion.
@JJMack - thank you for the feedback.
No, it was not designed to resize for web, all it does is create N amount of horizontal and vertical copies of the original layer. The user can always resize afterward to their desired size. The script is more general-purpose, this could be used for print output.
I agree that "Border (px)" may not be intuitive as that may set the expectation for a "stroke" – it is just an outer canvas margin, which is optional and can be left at the default zero value.
I can't agree though that "X copies" or "Y Copies" or "X Gap (px)" or "Y Gap (px)" is unitive. The GUI clearly states that the expectation is the number of copies from the original single layer, so yes, it should be 1 less than the total X or Y count.
I have taken your constructive criticism on board and have created a revised version where I have adjusted the GUI and hacked the underlying script code to work on the total desired layout required using "No. Across" and "No. Down" etc where if one types in 3 the total is 3.
/*
Layer Step & Repeat.jsx
v1.4 - Stephen Marsh, 8th April 2022
https://community.adobe.com/t5/photoshop-ecosystem-discussions/creating-duplicate-images-for-multi-packs-on-online-marketplaces-within-photoshop/td-p/12511723
*/
#target photoshop
/* ScriptUI Dialog */
// Dialog
var dialog = new Window("dialog");
dialog.text = "Layer Step & Repeat (v1.4)";
dialog.preferredSize.width = 280;
dialog.preferredSize.height = 135;
dialog.orientation = "row";
dialog.alignChildren = ["center", "top"];
dialog.spacing = 10;
dialog.margins = 10;
// Copies group
var copiesGroup = dialog.add("group", undefined, {
name: "copiesGroup"
});
copiesGroup.orientation = "column";
copiesGroup.alignChildren = ["left", "top"];
copiesGroup.spacing = 5;
copiesGroup.margins = 0;
var labelCopiesX = copiesGroup.add("statictext", undefined, undefined, {
name: "labelCopiesX"
});
labelCopiesX.text = "No. Across:";
/* Note: use editnumber instead of edittext for numerical fields! */
var copiesX = copiesGroup.add('editnumber {properties: {name: "copiesX"}}');
copiesX.text = "1";
copiesX.preferredSize.width = 50;
copiesX.alignment = ["fill", "top"];
// Preset the first field to be selected/active
copiesX.active = true;
var labelCopiesY = copiesGroup.add("statictext", undefined, undefined, {
name: "labelCopiesY"
});
labelCopiesY.text = "No. Down:";
var copiesY = copiesGroup.add('editnumber {properties: {name: "copiesY"}}');
copiesY.text = "1";
copiesY.preferredSize.width = 50;
copiesY.alignment = ["fill", "top"];
// Info footer text
/*
var infoGroup = copiesGroup.add("group", undefined, {name: "infoGroup"});
infoGroup.orientation = "column";
infoGroup.alignChildren = ["left","top"];
infoGroup.spacing = 10;
infoGroup.margins = [0,10,0,0];
infoGroup.alignment = ["left","top"];
var infoText = infoGroup.add("statictext", undefined, undefined, {name: "infoText"});
infoText.text = "Unused placeholder for possible future use";
*/
// Gap group
var gapGroup = dialog.add("group", undefined, {
name: "gapGroup"
});
gapGroup.orientation = "column";
gapGroup.alignChildren = ["left", "top"];
gapGroup.spacing = 5;
gapGroup.margins = 0;
var labelGapX = gapGroup.add("statictext", undefined, undefined, {
name: "labelGapX"
});
labelGapX.text = "Gap Across (px):";
var gapX = gapGroup.add('editnumber {properties: {name: "gapX"}}');
gapX.text = "0";
gapX.preferredSize.width = 50;
gapX.alignment = ["fill", "top"];
var labelGapY = gapGroup.add("statictext", undefined, undefined, {
name: "labelGapY"
});
labelGapY.text = "Gap Down (px):";
var gapY = gapGroup.add('editnumber {properties: {name: "gapY"}}');
gapY.text = "0";
gapY.preferredSize.width = 50;
gapY.alignment = ["fill", "top"];
// Outer margin group
var outerMarginGroup = dialog.add("group", undefined, {
name: "outerMarginGroup"
});
outerMarginGroup.orientation = "column";
outerMarginGroup.alignChildren = ["left", "top"];
outerMarginGroup.spacing = 5;
outerMarginGroup.margins = 0;
var labelOuterMargin = outerMarginGroup.add("statictext", undefined, undefined, {
name: "labelOuterMargin"
});
labelOuterMargin.text = "Outer Margin (px):";
var outerMargin = outerMarginGroup.add('editnumber {properties: {name: "outerMargin"}}');
outerMargin.text = "0";
outerMargin.preferredSize.width = 50;
outerMargin.alignment = ["fill", "top"];
var okButton = outerMarginGroup.add("button", undefined, undefined, {
name: "okButton"
});
okButton.text = "OK";
okButton.alignment = ["fill", "top"];
var cancelButton = outerMarginGroup.add("button", undefined, undefined, {
name: "cancelButton"
});
cancelButton.text = "Cancel";
cancelButton.alignment = ["fill", "top"];
/* Note: digits entered into fields are strings, not numbers! */
/* Main script */
var doc = activeDocument;
var docWidth = activeDocument.width.value;
var docHeight = activeDocument.height.value;
function main() {
// Ensure that the doc meets the script criteria
if (!app.activeDocument.activeLayer.isBackgroundLayer) {
// Render the GUI and OK button logic
if (dialog.show() === 1) {
// Call the function
arrayGenerator();
// Post script completion option A - call the Fit Image script
//$.evalFile(File(app.path.fsName + "/Presets/Scripts/Fit Image.jsx"));
// or
// Post script completion option B - run the image size command
//var idimageSize = stringIDToTypeID( "imageSize" );
//executeAction( idimageSize, undefined, DialogModes.ALL );
// End of script notification
app.beep();
} else {
//alert("Script cancelled!");
}
} else {
alert("This script is designed to only work on a non Background layer document!");
}
}
app.activeDocument.suspendHistory("Layer Step & Repeat", "main()");
/* Main script function */
function arrayGenerator() {
// Convert GUI variable strings to numbers...
// var copiesX2 = Math.floor(copiesX.text);
// The “double tilde” (~~) operator is a double NOT Bitwise operator. Use it as a substitute for Math.floor(), since it’s faster.
var copiesXX = ~~copiesX.text - 1;
var copiesXX = copiesXX.toString().replace(/^-\d+/, '0').replace(/\.\d+/, '');
var copiesX2 = ~~copiesXX;
var copiesYY = ~~copiesY.text - 1;
var copiesYY = copiesYY.toString().replace(/^-\d+/, '0').replace(/\.\d+/, '');
var copiesY2 = ~~copiesYY;
var gapX2 = ~~gapX.text;
var gapY2 = ~~gapY.text;
var outerMargin2 = ~~outerMargin.text;
// Only used for debugging
$.writeln(copiesX2);
$.writeln(copiesY2);
$.writeln(gapX2);
$.writeln(gapY2);
$.writeln(outerMargin2);
// Convert to % for relative canvas resize
var newCanvasX = copiesX2 * 100;
var newCanvasY = copiesY2 * 100;
var savedRuler = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// Relative % canvas resize
relativeCanvasSizePercent(true, newCanvasX, newCanvasY);
// Select all layers and group (hack to support multi-layered docs)
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
app.runMenuItem(stringIDToTypeID('groupLayersEvent'));
doc.activeLayer.name = "Original Layers";
// Step & repeat X
for (var i = 0; i < copiesX2; i++) {
copyToLayer();
movePX(docWidth + gapX2, 0);
}
// Select all layers
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
// Step & repeat Y
for (var i = 0; i < copiesY2; i++) {
copyToLayer();
movePX(0, docHeight + gapY2);
}
// Select all layers and group
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
app.runMenuItem(stringIDToTypeID('groupLayersEvent'));
doc.activeLayer.name = "Step & Repeat";
//renameOriginalLayersCopy();
// Extract original layers from step & repeat set
moveOriginalLayersSet();
deleteLayerSet();
// Absolute px canvas resize
doc.resizeCanvas(doc.width + gapX2 * copiesX2, doc.height + gapY2 * copiesY2, AnchorPosition.TOPLEFT);
// Optional outer margin absolute px canvas resize
doc.resizeCanvas(doc.width + outerMargin2 * 2, doc.height + outerMargin2 * 2, AnchorPosition.MIDDLECENTER);
app.preferences.rulerUnits = savedRuler;
/* Helper functions for main script */
/*
function renameOriginalLayersCopy() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var list = new ActionList();
var reference = new ActionReference();
reference.putName(s2t("layer"), "Original Layers copy");
descriptor.putReference(s2t("null"), reference);
descriptor.putBoolean(s2t("makeVisible"), false);
list.putInteger(296);
descriptor.putList(s2t("layerID"), list);
executeAction(s2t("select"), descriptor, DialogModes.NO);
app.activeDocument.activeLayer.name = "Original Layers copy 1";
}
*/
function moveOriginalLayersSet() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var list = new ActionList();
var reference = new ActionReference();
var reference2 = new ActionReference();
reference.putName(s2t("layer"), "Original Layers");
descriptor.putReference(s2t("null"), reference);
reference2.putIndex(s2t("layer"), 0);
descriptor.putReference(s2t("to"), reference2);
descriptor.putBoolean(s2t("adjustment"), false);
descriptor.putInteger(s2t("version"), 5);
list.putInteger(3);
descriptor.putList(s2t("layerID"), list);
executeAction(s2t("move"), descriptor, DialogModes.NO);
}
function deleteLayerSet() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var reference = new ActionReference();
reference.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
descriptor.putReference(s2t("null"), reference);
descriptor.putBoolean(s2t("deleteContained"), false);
executeAction(s2t("delete"), descriptor, DialogModes.NO);
}
function movePX(horizontal, vertical) {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var descriptor2 = new ActionDescriptor();
var reference = new ActionReference();
reference.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
descriptor.putReference(s2t("null"), reference);
descriptor2.putUnitDouble(s2t("horizontal"), s2t("pixelsUnit"), horizontal);
descriptor2.putUnitDouble(s2t("vertical"), s2t("pixelsUnit"), vertical);
descriptor.putObject(s2t("to"), s2t("offset"), descriptor2);
executeAction(s2t("move"), descriptor, DialogModes.NO);
}
function copyToLayer() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
executeAction(s2t("copyToLayer"), undefined, DialogModes.NO);
}
function relativeCanvasSizePercent(relative, width, height) {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
descriptor.putBoolean(s2t("relative"), relative);
descriptor.putUnitDouble(s2t("width"), s2t("percentUnit"), width);
descriptor.putUnitDouble(s2t("height"), s2t("percentUnit"), height);
descriptor.putEnumerated(s2t("horizontal"), s2t("horizontalLocation"), s2t("left"));
descriptor.putEnumerated(s2t("vertical"), s2t("verticalLocation"), s2t("top"));
executeAction(s2t("canvasSize"), descriptor, DialogModes.NO);
}
}
https://prepression.blogspot.com/2017/11/downloading-and-installing-adobe-scripts.html
Copy link to clipboard
Copied
An updated v1.5 including active layer checks for an unsuitable target layer (adjustment layers, solid fill layers, gradient fill layers, pattern fill layers or Background layer) and GUI options to run Fit Image or Image Size after the step and repeat (previously commented out as hard-coded options).
/*
Layer Step & Repeat.jsx
v1.5 - 13th October 2024, Stephen Marsh (Updated)
https://community.adobe.com/t5/photoshop-ecosystem-discussions/creating-duplicate-images-for-multi-packs-on-online-marketplaces-within-photoshop/td-p/12511723
*/
#target photoshop
// Check if there's an open document
if (app.documents.length === 0) {
alert("Please open a document before running this script.");
} else {
// Main dialog window
var dialog = new Window("dialog");
dialog.text = "Layer Step & Repeat (v1.5)";
dialog.preferredSize.width = 280;
dialog.preferredSize.height = 165;
dialog.orientation = "column";
dialog.alignChildren = ["fill", "top"]; // Changed to fill
dialog.spacing = 10;
dialog.margins = 10;
// Create panel to contain all interface elements except buttons
var panel = dialog.add("panel");
panel.orientation = "column";
panel.alignChildren = ["fill", "top"];
panel.spacing = 10;
panel.margins = 10;
// Main content group (now inside panel)
var mainGroup = panel.add("group");
mainGroup.orientation = "row";
mainGroup.alignChildren = ["left", "top"];
mainGroup.spacing = 10;
mainGroup.margins = 0;
// Copies group
var copiesGroup = mainGroup.add("group", undefined, {
name: "copiesGroup"
});
copiesGroup.orientation = "column";
copiesGroup.alignChildren = ["left", "top"];
copiesGroup.spacing = 5;
copiesGroup.margins = 0;
var labelCopiesX = copiesGroup.add("statictext", undefined, undefined, {
name: "labelCopiesX"
});
labelCopiesX.text = "No. Across:";
labelCopiesX.helpTip = "Total number of horizontal image layers, including the original layer";
var copiesX = copiesGroup.add('editnumber {properties: {name: "copiesX"}}');
copiesX.text = "1";
copiesX.preferredSize.width = 50;
copiesX.alignment = ["fill", "top"];
// Preset the first field to be selected/active
copiesX.active = true;
var labelCopiesY = copiesGroup.add("statictext", undefined, undefined, {
name: "labelCopiesY"
});
labelCopiesY.text = "No. Down:";
labelCopiesY.helpTip = "Total number of vertical image layers, including the original layer";
var copiesY = copiesGroup.add('editnumber {properties: {name: "copiesY"}}');
copiesY.text = "1";
copiesY.preferredSize.width = 50;
copiesY.alignment = ["fill", "top"];
// Gap group
var gapGroup = mainGroup.add("group", undefined, {
name: "gapGroup"
});
gapGroup.orientation = "column";
gapGroup.alignChildren = ["left", "top"];
gapGroup.spacing = 5;
gapGroup.margins = 0;
var labelGapX = gapGroup.add("statictext", undefined, undefined, {
name: "labelGapX"
});
labelGapX.text = "Gap Across (px):";
labelGapX.helpTip = "Horizontal image layer gap in pixels";
var gapX = gapGroup.add('editnumber {properties: {name: "gapX"}}');
gapX.text = "0";
gapX.preferredSize.width = 50;
gapX.alignment = ["fill", "top"];
var labelGapY = gapGroup.add("statictext", undefined, undefined, {
name: "labelGapY"
});
labelGapY.text = "Gap Down (px):";
labelGapY.helpTip = "Vertical image layer gap in pixels";
var gapY = gapGroup.add('editnumber {properties: {name: "gapY"}}');
gapY.text = "0";
gapY.preferredSize.width = 50;
gapY.alignment = ["fill", "top"];
// Outer margin group
var outerMarginGroup = mainGroup.add("group", undefined, {
name: "outerMarginGroup"
});
outerMarginGroup.orientation = "column";
outerMarginGroup.alignChildren = ["left", "top"];
outerMarginGroup.spacing = 5;
outerMarginGroup.margins = 0;
var labelOuterMargin = outerMarginGroup.add("statictext", undefined, undefined, {
name: "labelOuterMargin"
});
labelOuterMargin.text = "Outer Margin (px):";
labelOuterMargin.helpTip = "Optional canvas padding in pixels";
var outerMargin = outerMarginGroup.add('editnumber {properties: {name: "outerMargin"}}');
outerMargin.text = "0";
outerMargin.preferredSize.width = 50;
outerMargin.alignment = ["fill", "top"];
// Checkboxes group (now inside panel)
var checkboxGroup = panel.add("group");
checkboxGroup.orientation = "row";
checkboxGroup.alignChildren = ["left", "top"];
checkboxGroup.alignment = ["fill", "top"];
checkboxGroup.spacing = 10;
checkboxGroup.margins = 0;
var fitImageCheckbox = checkboxGroup.add("checkbox", undefined, "Run Fit Image");
fitImageCheckbox.helpTip = "Run the File > Automate > Fit Image command";
var imageSizeCheckbox = checkboxGroup.add("checkbox", undefined, "Run Image Size");
imageSizeCheckbox.helpTip = "Run the Image > Image Size command";
// Button group (outside panel)
var buttonGroup = dialog.add("group");
buttonGroup.orientation = "row";
buttonGroup.alignment = ["right", "top"];
buttonGroup.spacing = 10;
buttonGroup.margins = 0;
var cancelButton = buttonGroup.add("button", undefined, undefined, {
name: "cancelButton"
});
cancelButton.text = "Cancel";
var okButton = buttonGroup.add("button", undefined, undefined, {
name: "okButton"
});
okButton.text = "OK";
/* Main script */
var doc = app.activeDocument;
var docWidth = activeDocument.width.value;
var docHeight = activeDocument.height.value;
function main() {
// Ensure that the doc layer meets the script criteria
s2t = stringIDToTypeID;
(r = new ActionReference()).putProperty(s2t('property'), p = s2t('layerKind'));
r.putEnumerated(s2t('layer'), s2t('ordinal'), s2t('targetEnum'));
var layerKind = executeActionGet(r).getInteger(p);
// Adjustment layer || Gradient Fill layer || Pattern Fill layer || Solid Fill layer || Background layer
if (layerKind == 2 || layerKind == 9 || layerKind == 10 || layerKind == 11 || doc.activeLayer.isBackgroundLayer === true) {
alert("The current layer category isn't supported! If the layer is a Background layer, convert it to a standard layer.");
} else {
// Render the GUI and OK button logic
if (dialog.show() === 1) {
if (copiesX.text <= "1" && copiesY.text <= "1") {
alert("Script cancelled as both X & Y fields are set to 0 or 1!");
return; // Exit the script
}
// Call the function
arrayGenerator();
// Post script completion options
if (fitImageCheckbox.value) {
// Call the Fit Image script
$.evalFile(File(app.path.fsName + "/Presets/Scripts/Fit Image.jsx"));
}
if (imageSizeCheckbox.value) {
// Run the image size command
var idimageSize = stringIDToTypeID("imageSize");
executeAction(idimageSize, undefined, DialogModes.ALL);
}
// End of script notification
app.beep();
} else {
//alert("Script cancelled!");
}
}
}
app.activeDocument.suspendHistory("Layer Step & Repeat", "main()");
/* Main script function */
function arrayGenerator() {
// Convert GUI variable strings to numbers...
// var copiesX2 = Math.floor(copiesX.text);
// The "double tilde" (~~) operator is a double NOT Bitwise operator. Use it as a substitute for Math.floor(), since it's faster.
var copiesXX = ~~copiesX.text - 1;
var copiesXX = copiesXX.toString().replace(/^-\d+/, '0').replace(/\.\d+/, '');
var copiesX2 = ~~copiesXX;
var copiesYY = ~~copiesY.text - 1;
var copiesYY = copiesYY.toString().replace(/^-\d+/, '0').replace(/\.\d+/, '');
var copiesY2 = ~~copiesYY;
var gapX2 = ~~gapX.text;
var gapY2 = ~~gapY.text;
var outerMargin2 = ~~outerMargin.text;
// Only used for debugging
$.writeln(copiesX2);
$.writeln(copiesY2);
$.writeln(gapX2);
$.writeln(gapY2);
$.writeln(outerMargin2);
// Convert to % for relative canvas resize
var newCanvasX = copiesX2 * 100;
var newCanvasY = copiesY2 * 100;
var savedRuler = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// Relative % canvas resize
relativeCanvasSizePercent(true, newCanvasX, newCanvasY);
// Select all layers and group (hack to support multi-layered docs)
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
app.runMenuItem(stringIDToTypeID('groupLayersEvent'));
doc.activeLayer.name = "Original Layers";
// Step & repeat X
for (var i = 0; i < copiesX2; i++) {
copyToLayer();
movePX(docWidth + gapX2, 0);
}
// Select all layers
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
// Step & repeat Y
for (var i = 0; i < copiesY2; i++) {
copyToLayer();
movePX(0, docHeight + gapY2);
}
// Select all layers and group
app.runMenuItem(stringIDToTypeID('selectAllLayers'));
app.runMenuItem(stringIDToTypeID('groupLayersEvent'));
doc.activeLayer.name = "Step & Repeat";
// Having issues in later versions...
//renameOriginalLayersCopy();
// Extract original layers from step & repeat set
moveOriginalLayersSet();
deleteLayerSet();
// Ungroup the step & repeat layer sets
app.activeDocument.activeLayer = app.activeDocument.layers["Step & Repeat"];
ungroupLayers();
ungroupLayers();
// Absolute px canvas resize
doc.resizeCanvas(doc.width + gapX2 * copiesX2, doc.height + gapY2 * copiesY2, AnchorPosition.TOPLEFT);
// Optional outer margin absolute px canvas resize
doc.resizeCanvas(doc.width + outerMargin2 * 2, doc.height + outerMargin2 * 2, AnchorPosition.MIDDLECENTER);
app.preferences.rulerUnits = savedRuler;
}
/* Helper functions for main script */
function moveOriginalLayersSet() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var list = new ActionList();
var reference = new ActionReference();
var reference2 = new ActionReference();
reference.putName(s2t("layer"), "Original Layers");
descriptor.putReference(s2t("null"), reference);
reference2.putIndex(s2t("layer"), 0);
descriptor.putReference(s2t("to"), reference2);
descriptor.putBoolean(s2t("adjustment"), false);
descriptor.putInteger(s2t("version"), 5);
list.putInteger(3);
descriptor.putList(s2t("layerID"), list);
executeAction(s2t("move"), descriptor, DialogModes.NO);
}
function deleteLayerSet() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var reference = new ActionReference();
reference.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
descriptor.putReference(s2t("null"), reference);
descriptor.putBoolean(s2t("deleteContained"), false);
executeAction(s2t("delete"), descriptor, DialogModes.NO);
}
function movePX(horizontal, vertical) {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var descriptor2 = new ActionDescriptor();
var reference = new ActionReference();
reference.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
descriptor.putReference(s2t("null"), reference);
descriptor2.putUnitDouble(s2t("horizontal"), s2t("pixelsUnit"), horizontal);
descriptor2.putUnitDouble(s2t("vertical"), s2t("pixelsUnit"), vertical);
descriptor.putObject(s2t("to"), s2t("offset"), descriptor2);
executeAction(s2t("move"), descriptor, DialogModes.NO);
}
function copyToLayer() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
executeAction(s2t("copyToLayer"), undefined, DialogModes.NO);
}
function relativeCanvasSizePercent(relative, width, height) {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
descriptor.putBoolean(s2t("relative"), relative);
descriptor.putUnitDouble(s2t("width"), s2t("percentUnit"), width);
descriptor.putUnitDouble(s2t("height"), s2t("percentUnit"), height);
descriptor.putEnumerated(s2t("horizontal"), s2t("horizontalLocation"), s2t("left"));
descriptor.putEnumerated(s2t("vertical"), s2t("verticalLocation"), s2t("top"));
executeAction(s2t("canvasSize"), descriptor, DialogModes.NO);
}
function ungroupLayers() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var reference = new ActionReference();
reference.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
descriptor.putReference(s2t("null"), reference);
executeAction(s2t("ungroupLayersEvent"), descriptor, DialogModes.NO);
}
/*
function renameOriginalLayersCopy() {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var list = new ActionList();
var reference = new ActionReference();
reference.putName(s2t("layer"), "Original Layers copy");
descriptor.putReference(s2t("null"), reference);
descriptor.putBoolean(s2t("makeVisible"), false);
list.putInteger(296);
descriptor.putList(s2t("layerID"), list);
executeAction(s2t("select"), descriptor, DialogModes.NO);
app.activeDocument.activeLayer.name = "Original Layers copy 1";
}
*/
}
https://prepression.blogspot.com/2017/11/downloading-and-installing-adobe-scripts.html
Copy link to clipboard
Copied
The first thing you would need to do is crop your Image to some known aspect ratio. So you can scale the Images for the Web, Then you could Tile the image for your packages for web display. A one pack, a two pack, .... a twelve pack. Here I cropped your image to have a 2:3 Aspect ratio. I then wanted to create a 12 Pack for the web. Displays these days normally have at least a ppi resolution of 100ppi. So if I want a 12 pack to have 4 column and 3 rows 4x2" = 8" and 3x3" = 9" at 100 ppi the canvas size would be 800px by 900px and fit on most web devices displays. The Image most likely will be smaller the 8"x 9" many displays will have resolutions higher then 100ppi. So after I crop your image I saved it with its 2:3 Aspect Ratio. Then ran my Paste Image Roll script set to 100ppi 12 copies of images that are scaled to 2" x 3". I selected the saved 2:3 image.
Paste Image Roll Documentation
Copy link to clipboard
Copied
@defaultc46l0h0jo9o1 - Any feedback?
Copy link to clipboard
Copied
First response after almost 2 months is like shouting to emptiness.
Copy link to clipboard
Copied
Hah, didn't see the date stamp! Another hit and run...
Copy link to clipboard
Copied
Hi Stephen, I created this thread and completely forgot about it, apologies!
Thanks for creating the script, although there are a few things which I would like to add,
Firstly, online - all images should be 1000 x 1000 px (marketplace guidelines), therefore it would be great to have that be a part of the script, (that the canvas size is set to 1000 x 1000 px).
It would also make workflow / effeciancy much better if it was possible to open one image (of the product), and have the script run, but to create multiple versions with essentially one click (I have had actions set up in the past that are quite similar but im unsure as to how to integrate them into a script). It would mean that once you run the script on the image it exports multiple images, outputing a 2 pack, 3 pack, 4 pack, 6 pack, 12 pack main image and so on, into a folder, without having to run the script for each individual pack size.
It would be great if "trim" was added to the start of the script so that once the image is open as an object it removes the background, leaving just the content (product).
All pack sizes also need to be aligned centre to the 1000 x 1000 px canvas, with space around the edges, this would look more aesthetically pleasing than having each product ride along the edges.
some products would be more horizontally dominant than others, meaning that the space between each product should be vertically spaced more (the end result being a collection of products / pack size being more squared than rectangular and thus more suitable to be resized and aligned to the square canvas), ideally this could be split up into presets such as, extremely horizontal (a very narrow image vertically, but long horizontally), horizontal, neutral (square, neither horizontally or vertically dominant), vertical and extremely vertical.
I've attached an example of a product that is extremely horizontal.
Thanks
John
Copy link to clipboard
Copied
Hi John,
There is /*commented out*/ placeholder code to call the fit image or image size command. If the final step and repeat output of the script is required to be 1000x1000px with a white background, this could be hard-coded into the script.
The script is already providing more efficiency, however, I understand about workflow. You want it all automated for you with a single button press. The script was designed for user input and interactivity, while you are now stating that it should all be automated. Do you always need the same combination of 2 pack, 3 pack, 4 pack, 6 pack, 12 pack automatically generated? Or does the mix vary?
For the initial trim step, are the backgrounds transparent or *pure* white?
So how big would the stepped image be in px on the longest side? Say 900px, with a 50px margin either side for the 1000px total? Your original image samples were 1600px.
Ideally, you would supply before and after image examples for square, portrait and landscape products (3 x 2 before/after examples of a 6 pack). This way it is 100% clear what you require.
Copy link to clipboard
Copied
Hi Stephen,
Thanks for your reply.
I have checked out more of the combination/composite images from my competitor and it seems that they are arranging the layout of each of the packs dependent on orientation of the products.
For example:
6 Pack:
Extremely vertical: 6 across, 0 down
Vertical: 3 across, 2 down
Neutral: 3 across, 2 down
Horizontal: 3 across, 2 down
Extremely Horizontal: 0 across, 6 down.
It would make sense to have the initial product/object be resized within the 1000px canvas to the largest size as it will be more visible in the search results (the thumbnail of the image). Then leaving a gap/margin of about 50px around the edges.
All images (single packs) have a pure white background and the product sometimes has a drop shadow (nothing too extreme in terms of the shadow in most cases). In most cases the product images are downloaded/webscraped from other websites. All images would need to be trimmed down using the white pixels as the reference.
It depends on the shape of the product if the margins around the product/canvas should be 50px (see example of the bacofoil – 2 pack).
2 Pack,
No. Down 2, Gap down: 400px, canvas then resized to 1000x1000px, the gaps on the Y (top/bottom) are more suitable then if they we set to 50px (the products looked better arranged like this rather than having the vertical gap too much between the two).
3 Pack,
No. Down 3, Gap down: 200px, canvas then resized to 1000px and any transparent sections on the canvas filled with white.
4 Pack,
No. Down 4, Gap down: 50px canvas then resized to 1000px and any transparent sections on the canvas filled with white.
6 Pack,
No. Down 6, gap down: 0. Canvas then resized to 1000px (stack had to be resized down) and any transparent sections on the canvas filled were with white.
12 Pack,
I tried putting a gap of 100px between each product but the output was incorrect (some form of bug / error with the script?), I’ve attached the output jpg.
Coke Bottle (Vertical),
2 Pack,
No. Across: 2, Gap Across 100px, canvas resized to 1000x1000px, transparent filled with white.
3 Pack,
No. Across: 3, Gap Across 50px, canvas resized to 1000x1000px, transparent filled with white.
4 Pack,
No. Across 4, Gap Across 0. canvas resized to 1000x1000px, transparent filled with white. Products had to be resized and centred.
6 Pack,
No. Across 6, Gap Across 0, canvas resized to 1000x1000px and the product stack resized and centred.
12 Pack,
No. Across 6, No. Down 2, Gap 0, canvas resized 1000x1000px, products resized to fit.
Finish Powerball,
2 Pack,
No. Across 2, Gap 0, canvas/stack resized.
3 Pack,
No. Across 3, Gap 0, Canvas/stack resized.
4 Pack,
No. Across 2, No. Down 2, Canvas/stack resized.
6 Pack,
No. Across 3, No. Down 2, Canvas/stack resized.
12 Pack,
No. Across 4, No. Down 3, Canvas/stack resized.
I've attached the rest of the images in the next replies/comments.
Thanks,
John
Copy link to clipboard
Copied
Copy link to clipboard
Copied
Copy link to clipboard
Copied
All pack sizes would be 1, 2, 3, 4, 6, and 12.
Copy link to clipboard
Copied
@defaultc46l0h0jo9o1 – John, this is a very bespoke workflow and there are many variables. The scope and requirements have changed from the initial brief way too much for my liking. I apologise, but I have to be honest and blunt. Scripting is just a hobby for me, this feels too much like hard work, hard work that I wouldn't wish to do for money, let alone for free.
I might look into automating preset step/repeat combinations with hard-coded values, no GUI. This would be an extension of the initial work that I did previously based on the initial brief.
EDIT: John, please compare this 6up version attached to the Finish 6up sample that you provided. I can see this working, providing preset combinations of step and repeat. The source image would always have to be cropped correctly before being processed by the script. The script would basically just step and repeat with no margins/gutters, fit to 950px on the longest edge, then make the canvas square to 1000px resulting in an approx 25px outer margin. No variables, it is what it is, but it would automatically create the various 1, 2, 3, 4, 6, and 12 combinations with no variation.
Copy link to clipboard
Copied
@Stephen, your script works as intended, then it is just a matter to scale to the desired size.
Could the OP lauch your script via an action, set the number of copies, and then scale to size.
It always surprises me how demanding some can be with someone else's time, and they intend to make money out of it!!!
@defaultc46l0h0jo9o1 An action can do it as well: jump to new layer, set to percentage, canvas size, alt-drag a copy, repeat as desired, scale down as wanted. You'll learn a lot while experimenting!
Again from Trevor Morris: https://morris-photographics.com/photoshop/tutorials/actions.html
Copy link to clipboard
Copied
@defaultc46l0h0jo9o1 An action can do it as well: jump to new layer, set to percentage, canvas size, alt-drag a copy, repeat as desired, scale down as wanted. You'll learn a lot while experimenting!
Again from Trevor Morris: https://morris-photographics.com/photoshop/tutorials/actions.html
By @PECourtejoie
Agreed, it would only take 6 actions... Then a single action could play all 6 actions, and this could be batched or run from Image Processor or other batch scripts.
Probably quicker to just make the 6 actions than to code this as a script. The actions can be duped, so the 2up could be modified into the 3up etc. All one needs to do is use relative % sizing, transforms etc. so that the action is "generic" and not specific to absolute values, except for the fit image and canvas size which do need to be absolute px values.
Copy link to clipboard
Copied
Sometimes, the magic spells of half-god sorcerer-scripters can be replicated by the barbarian's simple actions 😉
Copy link to clipboard
Copied
Haha, but we both know that Actions aren't always simple, they can have hundreds of steps. They are just linear and lack the ability to use logic, variables, maths etc. I love it when an Action can do something creative that one may have thought was solely in the domain of scripting.
Copy link to clipboard
Copied
Hi Stephen,
Many thanks for that,
yes this would work, do you have the updated script for this?
Please check your PM, I'd like to send a few quid for the time.
@PECourtejoie I'm not intending to take the mick with anyones time here, if anyone doesn't want to do it I'm not forcing them, just FYI I did mention to Stephen prior to commenting that I would be happy to send a dew quid for his time.
Thanks
John
Copy link to clipboard
Copied
yes this would work, do you have the updated script for this?
No, I don't have the updated script John.
Before expending any time and effort, I wanted you to compare my proposed 6up vs. your 6up example. They are not exactly the same, but pretty close. As you have stated that this would work, I now know that you would be willing for this to be the basis of creating automated versions for 1, 2, 3, 4, 6, and 12 up combinations.
P.S. Thank you for your multiple private and public offers of remuneration, it is truly appreciated. This is just a hobby – I don't want it to feel like work. What I can do, I will, when I'm able.
Copy link to clipboard
Copied
Hi Stephen,
regarding your question, as you mentioned in your response re: the 6up version, a fully automated process albeit with no variables would definitely suffice.
I would suspect that both this new script and version 1.4 could be used in different circumstances, although I can see this new version becoming a valuable contribution to my business. So again, many thanks for your efforts.
John