The feature of exporting layers to PNG without trimming migrated from the File > Scripts > Export Layers to Files menu to File > Export > Layers to Files. Note however that's its usability is still terrible:
- It can't create new folders (will complain about destination not existing)
- It's prohibitively slow. It duplicates documents, adds and removes layers one by one and performs some other crazy actions I don't understand. It easily takes 10-20 seconds per layer.
I don't enjoy sitting in front of my screen for 10 minutes watching this script run, so I wrote a simple script:
if (!Array.from) {
Array.from = (function () {
var toStr = Object.prototype.toString;
var isCallable = function (fn) { return typeof fn === 'function' || toStr.call(fn) === '[object Function]' };
var toInteger = function (value) {
var number = Number(value);
if (isNaN(number))
return 0;
if (number === 0 || !isFinite(number))
return number;
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
};
var maxSafeInteger = Math.pow(2, 53) - 1;
var toLength = function (value) { return Math.min(Math.max(toInteger(value), 0), maxSafeInteger) };
return function from(arrayLike /*, mapFn, thisArg */) {
var c = this;
var items = Object(arrayLike);
if (arrayLike == null)
throw new TypeError("Array.from requires an array-like object - not null or undefined");
var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
var T;
if (typeof mapFn !== 'undefined') {
if (!isCallable(mapFn))
throw new TypeError("Array.from: when provided, the second argument must be a function");
if (arguments.length > 2)
T = arguments[2];
}
var len = toLength(items.length);
var a = isCallable(c) ? Object(new c(len)) : new Array(len);
var k = 0;
var kValue;
while (k < len) {
kValue = items[k];
a[k] = mapFn ? (typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k)) : kValue;
k += 1;
}
a.length = len;
return a;
};
})();
}
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
var k, o = Object(this), len = o.length >>> 0;
if (len === 0)
return -1;
var n = +fromIndex || 0;
if (Math.abs(n) === Infinity)
n = 0;
if (n >= len)
return -1;
k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
while (k < len) {
if (k in o && o[k] === searchElement)
return k;
k++;
}
return -1;
};
}
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (callback, thisArg) {
if (this === void 0 || this === null)
throw new TypeError('Array.prototype.forEach called on null or undefined');
if (callback.__class__ !== 'Function')
throw new TypeError(callback + ' is not a function');
var o = Object(this);
var len = o.length >>> 0;
var t = arguments.length > 1 ? thisArg : void 0;
for (var i = 0; i < len; i++)
if (i in o)
callback.call(t, o[i], i, o);
}
}
if (!Array.prototype.filter) {
Array.prototype.filter = function (callback, thisArg) {
if (this === void 0 || this === null)
throw new TypeError('Array.prototype.filter called on null or undefined');
if (callback.__class__ !== 'Function')
throw new TypeError(callback + ' is not a function');
var o = Object(this);
var len = o.length >>> 0;
var a = [];
var t = arguments.length > 1 ? thisArg : void 0;
for (var i = 0; i < len; i++) {
if (i in o) {
var val = o[i];
if (callback.call(t, val, i, o))
a.push(val);
}
}
return a;
};
}
if (!Array.prototype.some) {
Array.prototype.some = function (callback, thisArg) {
if (this === void 0 || this === null)
throw new TypeError('Array.prototype.some called on null or undefined');
if (callback.__class__ !== 'Function')
throw new TypeError(callback + ' is not a function');
var o = Object(this);
var len = o.length >>> 0;
var t = arguments.length > 1 ? thisArg : void 0;
for (var i = 0; i < len; i++)
if (i in o && callback.call(t, o[i], i, o))
return true;
return false;
};
}
if (!Array.prototype.map) {
Array.prototype.map = function (callback, thisArg) {
if (this === void 0 || this === null)
throw new TypeError('Array.prototype.map called on null or undefined');
if (callback.__class__ !== 'Function')
throw new TypeError(callback + ' is not a function');
var o = Object(this);
var len = o.length >>> 0;
var t = arguments.length > 1 ? thisArg : void 0;
var a = new Array(len);
for (var i = 0; i < len; i++)
if (i in o)
a[i] = callback.call(t, o[i], i, o);
return a;
};
}
if (!Array.prototype.flat) {
Array.prototype.flat = function (depth) {
if (this === void 0 || this === null)
throw new TypeError('Array.prototype.flat called on null or undefined');
depth = typeof depth === 'undefined' ? 1 : Number(depth) || 0;
var a = [];
var forEach = a.forEach;
var flatDeep = function (ai, di) {
forEach.call(ai,
function (val) {
if (di > 0 && Array.isArray(val))
flatDeep(val, di - 1);
else
a.push(val);
});
};
flatDeep(this, depth);
return a;
};
}
if (!String.prototype.repeat) {
String.prototype.repeat = function (count) {
var input = this;
var result = "";
for (var i = 0; i < count; i++)
result += input;
return result;
}
}
if (!String.prototype.padStart) {
String.prototype.padStart = function (targetLength, padString) {
targetLength = targetLength >> 0;
padString = String((typeof padString !== 'undefined' ? padString : ' '));
if (this.length > targetLength)
return String(this);
else {
targetLength = targetLength - this.length;
if (targetLength > padString.length)
padString += padString.repeat(targetLength / padString.length);
return padString.slice(0, targetLength) + String(this);
}
};
}
var id = {
name: 0,
as: 0,
method: 0,
quick: 0,
PNGInterlaceType: 0,
PNGInterlaceNone: 0,
PNGFilter: 0,
PNGFilterAdaptive: 0,
compression: 0,
minDigits: 0,
PNGFormat: 0,
quality: 0,
export_: 0,
reverse: 0,
animationFrameClass: 0,
startNumber: 0,
useDocumentSize: 0,
useDocumentFrameRate: 0,
allFrames: 0,
renderAlpha: 0,
alphaRendering: 0,
straight: 0,
Z3DPrefHighQualityErrorThreshold: 0,
videoExport: 0,
animationFramesFromLayers: 0,
apply: 0, // "Aply"
at: 0, // "At "
blendMode: 0, // "BlnM"
blue: 0, // "Bl "
channel: 0, // "Chnl"
color: 0, // "Clr "
commandID: 0,
delete_: 0, // "Dlt "
directory: 0,
enabled: 0, // "enab"
frameFill: 0, // "FrFl"
frameFX: 0, // "FrFX"
frameStyle: 0, // "FStl"
green: 0, // "Grn "
invokeCommand: 0,
kcanDispatchWhileModal: 0,
layer: 0, // "Lyr "
layerEffects: 0, // "Lefx"
layerStyle: 0,
level: 0, // "Lvl "
make: 0, // "Mk "
makeFrameAnimation: 0,
mask: 0, // "Msk "
mode: 0, // "Md "
new_: 0, // "Nw "
normal: 0, // "Nrml"
opacity: 0, // "Opct"
ordinal: 0, // "Ordn"
outsetFrame: 0, // "OutF"
overprint: 0,
paintType: 0, // "PntT"
percentUnit: 0, // "#Prc"
pixelsUnit: 0, // "#Pxl"
PNGMethod: 0,
present: 0,
property: 0, // "Prpr"
rasterizeItem: 0,
rasterizeLayer: 0,
red: 0, // "Rd "
RGBColor: 0, // "RGBC"
scale: 0, // "Scl "
sequenceRenderSettings: 0,
set: 0, // "setd"
showInDialog: 0,
size: 0, // "Sz "
solidColor: 0, // "SClr"
solidFill: 0, // "SoFi"
style: 0, // "Styl"
subdirectory: 0,
target: 0, // "null"
targetEnum: 0, // "Trgt"
thresholdClassEvent: 0, // "Thrs"
to: 0, // "T "
transparency: 0,
userMaskEnabled: 0,
using: 0, // "Usng"
what: 0 // "What"
};
function setupPhotoshopIdentifiers() {
for (var kid in id)
id[kid] = stringIDToTypeID(kid.replace("_", ""));
var classesWithPutMethods = {
ActionDescriptor: {
2: ['Boolean', 'Class', 'Double', 'Integer', 'LargeInteger', 'List', 'Path', 'Reference', 'String'],
3: ['Enumerated', 'Object', 'UnitDouble']
},
ActionReference: {
1: ['Class'],
2: ['Identifier', 'Index', 'Name', 'Offset', 'Property'],
3: ['Enumerated']
}
};
var chainMethods = {
1: function (f) {
return function (a0) {
this[f](a0);
return this;
};
},
2: function (f) {
return function (a0, a1) {
this[f](a0, a1);
return this;
};
},
3: function (f) {
return function (a0, a1, a2) {
this[f](a0, a1, a2);
return this;
};
}
}
for (var className in classesWithPutMethods) {
for (var argCount in classesWithPutMethods[className]) {
for (var iMethod in classesWithPutMethods[className][argCount]) {
var method = classesWithPutMethods[className][argCount][iMethod];
this[className].prototype['set' + method] = chainMethods[argCount]('put' + method);
}
}
}
}
function desc() {
return new ActionDescriptor();
}
function ref() {
return new ActionReference();
}
function execute(command, action) {
executeAction(command, action || desc(), DialogModes.NO);
}
setupPhotoshopIdentifiers();
var folderPostfix = "-layers";
function savePng(path) {
var opts = new ExportOptionsSaveForWeb();
opts.format = SaveDocumentType.PNG;
opts.PNG8 = false;
opts.transparency = true;
opts.interlaced = false;
opts.quality = 100;
doc.exportDocument(new File(path), ExportType.SAVEFORWEB, opts);
}
function makeFrameAnimation() {
execute(id.makeFrameAnimation);
}
function animationFramesFromLayers() {
execute(id.animationFramesFromLayers);
}
function reverseFrames() {
execute(id.reverse, desc()
.setReference(id.target, ref()
.setEnumerated(id.animationFrameClass, id.ordinal, id.targetEnum)));
}
function renderVideo(opts) {
var pngFormat = desc()
.setEnumerated(id.method, id.PNGMethod, id.quick)
.setEnumerated(id.PNGInterlaceType, id.PNGInterlaceType, id.PNGInterlaceNone)
.setEnumerated(id.PNGFilter, id.PNGFilter, id.PNGFilterAdaptive)
.setInteger(id.compression, 6);
var videoExport = desc()
.setObject(id.sequenceRenderSettings, id.sequenceRenderSettings,
desc().setObject(id.as, id.PNGFormat, pngFormat))
.setPath(id.directory, new File(opts.directory))
//.setString(id.subdirectory, opts.subdirectory)
.setString(id.name, ".png")
.setInteger(id.minDigits, 1)
.setInteger(id.startNumber, 0)
.setBoolean(id.useDocumentSize, true)
.setBoolean(id.useDocumentFrameRate, true)
.setBoolean(id.allFrames, true)
.setEnumerated(id.renderAlpha, id.alphaRendering, id.straight)
.setInteger(id.quality, 1)
.setInteger(id.Z3DPrefHighQualityErrorThreshold, 5);
execute(id.export_, desc()
.setObject(id.using, id.videoExport, videoExport));
}
function getArtLayers(doc) {
return Array.from(doc.artLayers);
}
function main() {
// Prepare output folder
var outFolder = new Folder(doc.path + "/" + doc.name.replace(".psd", folderPostfix));
if (!outFolder.exists)
outFolder.create();
// Copy visible layers into new document
var docCopy = app.activeDocument = doc.duplicate();
try {
getArtLayers(docCopy)
.filter(function(layer) { return !layer.visible })
.forEach(function(layer) { layer.remove() });
// Generate file names from layer names
var layerIndexLength = (docCopy.artLayers.length - 1 + "").length;
var formatLayerFileName = function(layer, iLayer) { return (iLayer + "").padStart(layerIndexLength, "0") + " " + layer.name };
var getOutPath = function(name) { return outFolder.fullName + "/" + name + ".png" };
var layerFileNames = getArtLayers(docCopy).map(formatLayerFileName).map(getOutPath);
// Confirm replacing existing files
var existingFiles = layerFileNames.filter(function(fileName) { return new File(fileName).exists });
if (existingFiles.length > 0 && !confirm(existingFiles.length + " files already exist in the target folder. Replace?"))
return;
// Export layers as video frames
makeFrameAnimation();
animationFramesFromLayers();
reverseFrames();
renderVideo({ directory: outFolder.fullName });
// Rename sequental file names to proper layer names
layerFileNames
.forEach(function(fileName, iLayer) {
new File(fileName).remove();
new File(getOutPath(iLayer)).rename(fileName);
});
} finally {
// Close document copy without saving
docCopy.close(SaveOptions.DONOTSAVECHANGES);
app.activeDocument = doc;
}
alert("Layers exported.");
}
var doc = app.activeDocument;
//doc.suspendHistory('Athari - Export Layers', 'main()');
main();
It doesn't provide a fancy user interface and probably fails in many cases, but it gets the job done 5 times faster. Feel free to adapt it to your needs.
Update
Based on suggestion from @joojaa, replaced export to PNG with export to PNG video frames. Now it's 5 times faster than before (3 layers per second in my case), doesn't create an entry in the undo history and even shows a progress bar. (I copy-pasted my includes right into the script, so it may look very long, but the actual code is just one page at the very end.)