/*
Save Instagram 1080px Carousel Images v1-3.jsx
Stephen Marsh
v1.0, 8th January 2025
v1.1, 14th January 2025 - Added doc height checking in addition to the previous width checks
v1.2, 23rd March 2025 - Added a check if the final parent layer is an artboard. Added a check for the folder selection on OK.
Info: This script saves PNG, JPEG, or WEBP "slices" of an Instagram carousel image at 1080px wide,
without relying on the slice tool or Save for Web (Legacy) export functions.
Inspired by
https://community.adobe.com/t5/photoshop-ecosystem-discussions/exporting-carousels-ruined/td-p/15073529
*/
#target photoshop
(function () {
// Check if there is an active document
if (!app.documents.length) {
alert("Error: No document is open.\nPlease open a document and try again.");
return; // Abort script
}
// Set the active document
var doc = app.activeDocument;
// Check if the active document is saved
try {
doc.path;
} catch (err) {
alert("Error: The document must be saved before running this script.\nPlease save the document and try again.");
return; // Abort script
}
// Check document width and height requirements
var docWidthPx = doc.width.as('px'); // Explicitly convert to pixels
var docHeightPx = doc.height.as('px'); // Explicitly convert to pixels
// Check width requirements
if (docWidthPx < 2160) {
alert("Error: Document width must be at least 2160 pixels (2 frames).\nCurrent width: " + docWidthPx + "px");
return; // Abort script
}
if (docWidthPx % 1080 !== 0) {
alert("Error: Document width must be divisible by 1080 pixels.\nCurrent width: " + docWidthPx + "px");
return; // Abort script
}
// Check document height requirements - don't use || because it will always evaluate to true!
if (docHeightPx !== 1080 && docHeightPx !== 1350) {
alert("Error: Document height must be either 1080 pixels or 1350 pixels.\nCurrent height: " + docHeightPx + "px");
return; // Abort script
}
// Selected layer check, based on code by jazz-y
s2t = stringIDToTypeID;
(r = new ActionReference()).putProperty(s2t('property'), p = s2t('targetLayers'));
r.putEnumerated(s2t("document"), s2t("ordinal"), s2t("targetEnum"));
if (!executeActionGet(r).getList(p).count) {
alert('A layer must be selected!');
return; // Abort script
}
// Store all information needed for artboard check
var layerInfo = collectLayerInfo();
// Check if any parent is an artboard
if (checkForArtboardParent(layerInfo)) {
alert("Script aborted!\nPlease select a layer or group that is not within an Artboard and try again.");
return; // Abort script
}
// Create the dialog window
var dialog = new Window("dialog", "Save Instagram 1080px Carousel Images (v1.2)");
dialog.orientation = "column";
dialog.preferredSize.width = 500;
dialog.alignChildren = "fill";
// Create a panel for the GUI elements
var panel = dialog.add("panel", undefined, "");
panel.orientation = "column";
panel.alignChildren = "left";
var infoText = panel.add("statictext", undefined, "Note: Files will be saved as sRGB 8 bits/channel. Artboards are not supported.");
infoText.alignment = "left";
// Save location button and field
var locationGroup = panel.add("group");
locationGroup.add("statictext", undefined, "Save Location:");
var browseButton = locationGroup.add("button", undefined, "Browse...");
var locationInput = locationGroup.add("statictext", undefined, "No folder selected", { truncate: "middle" });
locationInput.characters = 30;
browseButton.onClick = function () {
var selectedFolder = Folder.selectDialog("Select Save Location");
if (selectedFolder) {
locationInput.text = selectedFolder.fsName;
}
};
// Create a conditional file format dropdown
var formats = ["JPEG", "PNG"];
if (parseFloat(app.version) >= 23) {
formats.push("WEBP");
}
// File format dropdown
var formatGroup = panel.add("group");
formatGroup.add("statictext", undefined, "File Format:");
var formatDropdown = formatGroup.add("dropdownlist", undefined, formats);
formatDropdown.selection = 0; // Default to the first format
// OK and Cancel buttons
var buttonGroup = dialog.add("group");
buttonGroup.alignment = "right";
var cancelButton = buttonGroup.add("button", undefined, "Cancel", { name: "cancel" });
var okButton = buttonGroup.add("button", undefined, "OK", { name: "ok" });
okButton.onClick = function () {
if (locationInput.text === "No folder selected") {
alert("Error: No folder selected.\nPlease select a folder and try again.");
return;
}
dialog.close(1);
};
cancelButton.onClick = function () {
dialog.close(0);
};
// Show the dialog
if (dialog.show() != 1) {
return; // User canceled
}
// Get user input
var selectedFormat = formatDropdown.selection.text;
var saveLocation = new Folder(locationInput.text);
// Function to save slices
function saveImages() {
var originalWidth = doc.width;
var frameWidth = 1080;
var frameCount = Math.floor(originalWidth / frameWidth);
var docName = doc.name.replace(/\.[^\.]+$/, ''); // Remove file extension
for (var i = 0; i < frameCount; i++) {
doc.duplicate(false);
var duplicateDoc = app.activeDocument;
// Bitmap mode input
if (activeDocument.mode == DocumentMode.BITMAP) {
activeDocument.changeMode(ChangeMode.GRAYSCALE);
activeDocument.changeMode(ChangeMode.RGB);
activeDocument.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false);
activeDocument.bitsPerChannel = BitsPerChannelType.EIGHT;
// Indexed Color, CMYK or Lab mode input
} else if (activeDocument.mode == DocumentMode.INDEXEDCOLOR || activeDocument.mode == DocumentMode.CMYK || activeDocument.mode == DocumentMode.LAB) {
activeDocument.changeMode(ChangeMode.RGB);
activeDocument.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false);
activeDocument.bitsPerChannel = BitsPerChannelType.EIGHT;
} else {
activeDocument.changeMode(ChangeMode.RGB);
activeDocument.convertProfile("sRGB IEC61966-2.1", Intent.RELATIVECOLORIMETRIC, true, false);
activeDocument.bitsPerChannel = BitsPerChannelType.EIGHT;
}
// Dupe the layer as a brute force metadata removal step
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var reference = new ActionReference();
var reference2 = new ActionReference();
reference.putClass(s2t("document"));
descriptor.putReference(s2t("null"), reference);
descriptor.putString(s2t("name"), docName);
reference2.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
descriptor.putReference(s2t("using"), reference2);
executeAction(s2t("make"), descriptor, DialogModes.NO);
// Close the original duped doc
duplicateDoc.close(SaveOptions.DONOTSAVECHANGES);
// Set the layer duped doc as the new active document
var duplicateDoc = app.activeDocument;
var left = frameWidth * i;
var right = frameWidth * (i + 1);
// Create a new crop area for each frame/slice
duplicateDoc.crop([left, 0, right, duplicateDoc.height]);
var fileName = docName + "_slice_" + (i + 1) + "." + selectedFormat.toLowerCase();
var file = new File(saveLocation + "/" + fileName);
switch (selectedFormat) {
case "JPEG":
var jpgSaveOptions = new JPEGSaveOptions();
jpgSaveOptions.embedColorProfile = true;
jpgSaveOptions.formatOptions = FormatOptions.STANDARDBASELINE;
jpgSaveOptions.matte = MatteType.NONE;
jpgSaveOptions.quality = 10; // Low to high quality level: 0-12
duplicateDoc.saveAs(file, jpgSaveOptions, true, Extension.LOWERCASE);
break;
case "PNG":
// Use AM code as DOM code doesn't embed the ICC profile!
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var descriptor2 = new ActionDescriptor();
descriptor2.putEnumerated(s2t("method"), s2t("PNGMethod"), s2t("quick"));
descriptor2.putEnumerated(s2t("PNGInterlaceType"), s2t("PNGInterlaceType"), s2t("PNGInterlaceNone"));
descriptor2.putEnumerated(s2t("PNGFilter"), s2t("PNGFilter"), s2t("PNGFilterAdaptive"));
descriptor2.putInteger(s2t("compression"), 1); // High to low quality level: 0-9
descriptor2.putEnumerated(s2t("embedIccProfileLastState"), s2t("embedOff"), s2t("embedOn"));
descriptor.putObject(s2t("as"), s2t("PNGFormat"), descriptor2);
descriptor.putPath(s2t("in"), new File(file));
descriptor.putBoolean(s2t("copy"), true);
descriptor.putBoolean(s2t("lowerCase"), true);
descriptor.putBoolean(s2t("embedProfiles"), true);
executeAction(s2t("save"), descriptor, DialogModes.NO);
break;
case "WEBP":
// Call the saveAsWebP function
saveAsWebP(file);
break;
}
// Close the temporary document
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}
// End of script notification
alert("Script completed successfully!\n" + frameCount + " slices saved to:\n" + saveLocation);
}
// Save as WebP using AM code
function saveAsWebP(file) {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var descriptor = new ActionDescriptor();
var descriptor2 = new ActionDescriptor();
descriptor2.putEnumerated(s2t("compression"), s2t("WebPCompression"), s2t("compressionLossy")); // "compressionLossy" or "compressionLossless"
descriptor2.putInteger(s2t("quality"), 75); // 0 Low to 100 high image quality, only valid for "compressionLossy"
descriptor2.putBoolean(s2t("includeXMPData"), false); // boolean
descriptor2.putBoolean(s2t("includeEXIFData"), false); // boolean
descriptor2.putBoolean(s2t("includePsExtras"), false); // boolean
descriptor.putObject(s2t("as"), s2t("WebPFormat"), descriptor2);
descriptor.putPath(s2t("in"), file);
descriptor.putBoolean(s2t("copy"), true);
descriptor.putBoolean(s2t("lowerCase"), true);
descriptor.putBoolean(s2t("embedProfiles"), true); // boolean
executeAction(s2t("save"), descriptor, DialogModes.NO);
}
function collectLayerInfo() {
var info = [];
var currentLayer = doc.activeLayer;
// Store information about the active layer and its ancestry
while (currentLayer && currentLayer !== doc) {
try {
info.push({
name: currentLayer.name,
id: currentLayer.id
});
currentLayer = currentLayer.parent;
} catch (e) {
break; // Exit if we can't go further up the layer hierarchy
}
}
return info;
}
function isArtboard(layerId) {
try {
var s2t = function (s) {
return app.stringIDToTypeID(s);
};
var ref = new ActionReference();
ref.putIdentifier(s2t("layer"), layerId);
var desc = executeActionGet(ref);
// Check if this layer has the artboard property
return desc.hasKey(s2t("artboard"));
} catch (e) {
return false;
}
}
function checkForArtboardParent(layerInfo) {
for (var i = 0; i < layerInfo.length; i++) {
if (isArtboard(layerInfo[i].id)) {
return true;
}
}
return false;
}
// Run the main function to save the slices
saveImages();
}());
- Copy the code text to the clipboard
- Open a new blank file in a plain-text editor (not in a word processor)
- Paste the code in
- Save as a plain text format file – .txt
- Rename the saved file extension from .txt to .jsx
- Install or browse to the .jsx file to run (see below)
https://prepression.blogspot.com/2017/11/downloading-and-installing-adobe-scripts.html