Copy link to clipboard
Copied
Hi,
I've tried to work a little with photoshop scripting, and I tried to make a thing that removes groups if it fits some criteria.
The problem I am facing is - it's VERY VERY slow. Let's say we have a file that contains 10 groups, each group contains 10 sub-groups, and each of these groups contains 10 layers.
I made a recursion that checks if the first child layer is a group, if it is a group, it moves in (recursion), if it's not, then skip. At the end, remove group. (This is simplified, but it is basically the thing).
From my previous programming experience, I'd say this should run under a second without any issues. But what I've found out is, that it takes several seconds to remove one group.
This is just straight up unacceptable. Can anyone point me in the right direction on how to solve this? Or explain to me, why is such a popular app so so so badly made? (In this specific case)
These results show, that any photoshop call takes quite a long time. Any suggestions on how to improve this? I'd need it to improve dramatically in speed (at least 10x).
Second issue:
I've now noticed a second problem - the first group is marked visible immediatelly, so it doens't get removed. Any idea why this happens?
Intel core i7-7700, 32 GB RAM, Adobe Photoshop 2021
Recreate demo:
Photoshop file:
create new, printing, A4, remove layer, add empty layer, dulicate so you have 10 empty layers. Now group these 10 layers into one group, and duplicate 9 more times. Group these groups together and duplicate 9 more times.
So there are 10 top groups, each contains 10 sub groups, each sub group contains 10 empty groups.
My code:
var start_time = new Date().getTime();
var doc = activeDocument;
var objectString;
var textFile = File(doc.path + '/time.txt');
textFile.encoding = 'UTF-8';
textFile.open('a');
textFile.write("Initialization: ", new Date().getTime() - start_time, "\n");
loopTopGroups();
textFile.write("Total time: ", new Date().getTime() - start_time, "\n");
function loopTopGroups() {
var time_before = new Date().getTime();
var len = doc.layers.length;
var layers = doc.layers;
textFile.write("Get layers: ", new Date().getTime() - time_before, "\n");
for (var i = len - 1; i >= 0; i--) {
var currentLayer = layers[i];
if (!currentLayer.visible) {
loopGroups(currentLayer);
}
if (currentLayer.layers.length == 0) {
var time_before = new Date().getTime();
currentLayer.remove();
textFile.write("Out remove: ", new Date().getTime() - time_before, "\n");
}
}
}
function loopGroups(parentLayer) {
var time_before = new Date().getTime();
var len = parentLayer.layers.length;
var layers = parentLayer.layers;
textFile.write("Get inner layers: ", new Date().getTime() - time_before, "\n");
for (var i = len - 1; i >= 0; i--) {
var currentLayer = layers[i];
if (currentLayer.typename == 'LayerSet') {
if (currentLayer.layers[0].typename == 'LayerSet') {
loopGroups(currentLayer);
} else {
var time_before = new Date().getTime();
currentLayer.remove();
textFile.write("Remove: ", new Date().getTime() - time_before, "\n");
}
}
}
}
Output:
my final target was to have a script that removes not needed groups and ignores entire group if it's visible on top level (no recursion dive into these groups).
how to determine if the group is not needed - if it's first sub-layer is a layer and not a group, and the name of this group is not in a .txt file, remove.
By @Samuel_PFD
UPD 1*
#target photoshop
$.hiresTimer
var lr = new AM(),
namesOfGroupsFromTxtFile = ['test name 1', 'test name 2']
var lrs = getLayersCollection(),
exceptionN
...
After hours of pain and suffering, I made my own solution.
@jazz-y I really appreciate you helping (and your older posts where you explained some basics and how to start), without you, I wouldn't have been able to do anything.
I would have straight up used your solution, but it had some errors, and at that time, I had no knowledge of this magic API, so I wasn't capable of fixing it -> so I learned the basics and adapted them in my solution.
Again, huge thanks to you.
loopGroups(loadObjectsFile(
...
After some talk with my team, the target changed a little, to make it easier to work with.
Right now what the script is supposed to do - ignore groups that are visible on top level.
For the rest of the groups, find those that are in the objects.txt file. Remove everything else (except parent groups of groups to keep). It also deletes the top groups first to make it faster.
Huge thanks to this community, I wouldn't have been able to make it at all without you.
removeGroups(loadObjectsFile());
//
...
Copy link to clipboard
Copied
To significantly speed up the script's work, you need to use the Action Manager code.
So I understand that besides the slow work, your script also does not work exactly as you wanted. Briefly describe what result of this script you would like to get?
Copy link to clipboard
Copied
Any idea why regular old coding doesn't produce up to speed results?
Any references toward the Action Manager code?
Right now, I want the script to remove all hidden groups. The problem is that when it starts, it makes the first hidden group visible, so it won't remove it later on.
Thanks for your input.
Copy link to clipboard
Copied
In simple words, when working with the DOM, the script receives all the properties of an object at the time of accessing it (even if you don't need them). Action manager allows you to get only those properties that are needed at the moment. This greatly speeds up the script. I'm busy at the moment, but a little later I can give you a sample code that removes invisible groups. Perhaps other users will help you too
Copy link to clipboard
Copied
my final target was to have a script that removes not needed groups and ignores entire group if it's visible on top level (no recursion dive into these groups).
how to determine if the group is not needed - if it's first sub-layer is a layer and not a group, and the name of this group is not in a .txt file, remove.
that's it
I didn't include this checking in the demo because it wasn't needed to show the speed handicap.
Copy link to clipboard
Copied
my final target was to have a script that removes not needed groups and ignores entire group if it's visible on top level (no recursion dive into these groups).
how to determine if the group is not needed - if it's first sub-layer is a layer and not a group, and the name of this group is not in a .txt file, remove.
By @Samuel_PFD
UPD 1*
#target photoshop
$.hiresTimer
var lr = new AM(),
namesOfGroupsFromTxtFile = ['test name 1', 'test name 2']
var lrs = getLayersCollection(),
exceptionNames = {};
do { exceptionNames[namesOfGroupsFromTxtFile.shift()] = true } while (namesOfGroupsFromTxtFile.length)
selectAndDelete(lr, getNotNeededGroups(lrs, exceptionNames))
var lrs = getLayersCollection(),
emptyTopLevelGroups = [];
for (var i = 0; i < lrs.length; i++) if (lrs[i].type == 7 && !lrs[i].length) emptyTopLevelGroups.push(lrs[i].id)
selectAndDelete(lr, emptyTopLevelGroups)
var end = $.hiresTimer
alert(end / 1000000)
function getLayersCollection() {
var doc = new AM('document'),
lr = new AM('layer'),
indexFrom = doc.getProperty('hasBackgroundLayer') ? 0 : 1,
indexTo = doc.getProperty('numberOfLayers');
return layersCollection(indexFrom, indexTo)
function layersCollection(from, to, parentItem, group) {
parentItem = parentItem ? parentItem : [];
for (var i = from; i <= to; i++) {
var layerSectionType = lr.getProperty('layerSection', i, true).value;
if (layerSectionType == 'layerSectionEnd') {
i = layersCollection(i + 1, to, [], parentItem)
continue;
}
var properties = {};
properties.name = lr.getProperty('name', i, true)
properties.id = lr.getProperty('layerID', i, true)
properties.visible = lr.getProperty('visible', i, true)
properties.type = lr.getProperty('layerKind', i, true)
if (layerSectionType == 'layerSectionStart') {
for (o in properties) { parentItem[o] = properties[o] }
group.push(parentItem);
return i;
} else {
parentItem.push(properties)
}
}
return parentItem
}
}
function getNotNeededGroups(lrs, exceptionNames, output) {
output = output ? output : []
exceptionNames = exceptionNames ? exceptionNames : {}
for (var i = 0; i < lrs.length; i++) {
var cur = lrs[i];
if (!exceptionNames[cur.name] && cur instanceof Array) {
if (cur.length) {
if (cur[0].type != 7) {
output.push(cur.id);
} else {
if (cur.visible) continue;
getNotNeededGroups(cur, exceptionNames, output)
}
}
}
}
return output
}
function selectAndDelete(lr, list) {
if (list.length) {
lr.deselect()
lr.selectLayers(list)
lr.removeLayer()
}
}
function AM(target, order) {
var s2t = stringIDToTypeID,
t2s = typeIDToStringID;
target = target ? s2t(target) : null;
this.getProperty = function (property, id, idxMode) {
property = s2t(property);
(r = new ActionReference()).putProperty(s2t('property'), property);
id != undefined ? (idxMode ? r.putIndex(target, id) : r.putIdentifier(target, id)) :
r.putEnumerated(target, s2t('ordinal'), order ? s2t(order) : s2t('targetEnum'));
return getDescValue(executeActionGet(r), property)
}
this.hasProperty = function (property, id, idxMode) {
property = s2t(property);
(r = new ActionReference()).putProperty(s2t('property'), property);
id ? (idxMode ? r.putIndex(target, id) : r.putIdentifier(target, id))
: r.putEnumerated(target, s2t('ordinal'), order ? s2t(order) : s2t('targetEnum'));
return executeActionGet(r).hasKey(property)
}
this.descToObject = function (d) {
var o = {}
for (var i = 0; i < d.count; i++) {
var k = d.getKey(i)
o[t2s(k)] = getDescValue(d, k)
}
return o
}
this.removeLayer = function (list) {
r = new ActionReference;
if (list) { do { r.putIdentifier(s2t('layer'), list.shift()) } while (list.length) } else {
r.putEnumerated(s2t('layer'), s2t('ordinal'), s2t('targetEnum'))
}
(d = new ActionDescriptor()).putReference(s2t('null'), r);
executeAction(s2t('delete'), d);
}
this.deselect = function () {
(r = new ActionReference()).putEnumerated(s2t('layer'), s2t('ordinal'), s2t('targetEnum'));
(d = new ActionDescriptor()).putReference(s2t('null'), r);
executeAction(s2t('selectNoLayers'), d, DialogModes.NO);
}
this.selectLayers = function (list) {
var r = new ActionReference();
do { r.putIdentifier(s2t('layer'), list.shift()) } while (list.length);
(d = new ActionDescriptor()).putReference(s2t('null'), r);
d.putEnumerated(s2t('selectionModifier'), s2t('addToSelectionContinuous'), s2t('addToSelection'));
executeAction(s2t('select'), d, DialogModes.NO);
}
function getDescValue(d, p) {
switch (d.getType(p)) {
case DescValueType.OBJECTTYPE: return { type: t2s(d.getObjectType(p)), value: d.getObjectValue(p) };
case DescValueType.LISTTYPE: return d.getList(p);
case DescValueType.REFERENCETYPE: return d.getReference(p);
case DescValueType.BOOLEANTYPE: return d.getBoolean(p);
case DescValueType.STRINGTYPE: return d.getString(p);
case DescValueType.INTEGERTYPE: return d.getInteger(p);
case DescValueType.LARGEINTEGERTYPE: return d.getLargeInteger(p);
case DescValueType.DOUBLETYPE: return d.getDouble(p);
case DescValueType.ALIASTYPE: return d.getPath(p);
case DescValueType.CLASSTYPE: return d.getClass(p);
case DescValueType.UNITDOUBLE: return (d.getUnitDoubleValue(p));
case DescValueType.ENUMERATEDTYPE: return { type: t2s(d.getEnumerationType(p)), value: t2s(d.getEnumerationValue(p)) };
default: break;
};
}
}
* I reread your comment and updated the code. Perhaps I misunderstood something. In any case, you can modify it yourself to suit your needs
Copy link to clipboard
Copied
The puzzle to you: what this unique statement does:
activeDocument.activeLayer.visible ^= 1
Copy link to clipboard
Copied
XOR, toggles layer visiblity 🙂
1 ^= 1 == 0
0 ^= 1 == 1
Copy link to clipboard
Copied
You are the winner, but mostly not many knows it, did you?
Copy link to clipboard
Copied
yes, what did you aim to show with this?
Copy link to clipboard
Copied
Inserting it in proper spot of your original code would solve the problem of making visible the top layerSet, so that you wanted to avoid.
Copy link to clipboard
Copied
Posting cropped screenshots of the fully expanded layers panel in a before/after would help to illustrate.
Copy link to clipboard
Copied
would help illustrate ... what?
before: 10 groups, each has 10 subgroups, each has 10 layers.
after: 1 group, with 10 subgroups, each with 10 layers
Copy link to clipboard
Copied
Edit:
before - all top groups are hidden
after - the only top group remaining is visible
Copy link to clipboard
Copied
If all top groups are hidden then how you know which one has to remian visible?
Copy link to clipboard
Copied
I didn't know there had to stay one group - you can't just remove all.
Let's say there will always be at least one visible group (yes I know this is not the correct way to handle this, but yeah ...)
Copy link to clipboard
Copied
Yes and it's why there is unaware mistake in the description: "remove layer, add empty layer".
Until you create transparent layer you can't in first instance remove layer to create new one 😉
Copy link to clipboard
Copied
My version is with no recursion that surely would be helpful in other layerSets tree. You may use $.hiresTimer at beginning and end of code to check it takes a half of second. Use fully AM to speed it up:
sTT = stringIDToTypeID, lSs = [].slice.call(activeDocument.layerSets, o = {})
.sort(function(v){(vsble = v.visible) && o[v.id] = ''; return !vsble}).splice(o.__count__)
ref=new ActionReference(); while(lSs.length) ref.putIdentifier(sTT('layer'),lSs.shift().id);
(dsc=new ActionDescriptor()).putReference(sTT('null'), ref),executeAction(sTT('delete'),dsc)
Copy link to clipboard
Copied
After hours of pain and suffering, I made my own solution.
@jazz-y I really appreciate you helping (and your older posts where you explained some basics and how to start), without you, I wouldn't have been able to do anything.
I would have straight up used your solution, but it had some errors, and at that time, I had no knowledge of this magic API, so I wasn't capable of fixing it -> so I learned the basics and adapted them in my solution.
Again, huge thanks to you.
loopGroups(loadObjectsFile());
// Load objects.txt file
function loadObjectsFile() {
var title = getProperty('document', 'title');
var full_path = String(getProperty('document', 'fileReference')).split('%20').join(' ');
var textFile = File(full_path.replace(title, 'objects.txt'));
textFile.encoding = 'UTF-8';
textFile.open('r');
return textFile.read().toLowerCase();
}
// Return if top group is visible
function isTopVisible(index) {
var parentID = getLayerPropertyByIndex(index, 'parentLayerID');
var ID = getLayerPropertyByIndex(index, 'layerID');
while (parentID != -1) {
ID = getLayerPropertyByID(ID, 'parentLayerID');
parentID = getLayerPropertyByID(ID, 'parentLayerID');
}
return getLayerPropertyByID(ID, 'visible');
}
function arrayContains(array, element) {
for (var i = 0; i < array.length; i++) {
if (array[i] == element) {
return true;
}
}
return false;
}
function loopGroups(objectString) {
var start = getProperty('document', 'hasBackgroundLayer') ? 0 : 1;
var end = getProperty('document', 'numberOfLayers');
var markedIDs = [];
for (var i = end - 1; i >= start; i--) {
var ID = getLayerPropertyByIndex(i, 'layerID');
if (arrayContains(markedIDs, ID)) {
continue;
}
var section = getLayerPropertyByIndex(i, 'layerSection').value;
var removed = false;
// It's a group
if (section == 'layerSectionStart') {
if (!isTopVisible(i)) {
var name = getLayerPropertyByIndex(i, 'name');
if (objectString.indexOf(String(name).toLowerCase()) == -1) {
removeLayerByID(ID);
i = getProperty('document', 'numberOfLayers');
removed = true;
}
}
}
if (!removed) {
markedIDs.push(ID);
}
}
}
function s2t(s) { return stringIDToTypeID(s); }
function t2s(t) { return typeIDToStringID(t); }
function getProperty(obj, property) {
var ref = new ActionReference();
ref.putProperty(s2t('property'), s2t(property));
ref.putEnumerated(s2t(obj), s2t('ordinal'), s2t('targetEnum'));
return getDescValue(executeActionGet(ref), s2t(property));
}
function getLayerPropertyByIndex(index, property) {
ref = new ActionReference();
ref.putProperty(s2t('property'), s2t(property))
ref.putIndex(s2t('layer'), index);
var desc = executeActionGet(ref);
return getDescValue(desc, s2t(property));
}
function getLayerPropertyByID(ID, property) {
ref = new ActionReference();
ref.putProperty(s2t('property'), s2t(property))
ref.putIdentifier(s2t('layer'), ID);
var desc = executeActionGet(ref);
return getDescValue(desc, s2t(property));
}
function removeLayerByID(ID) {
ref = new ActionReference();
ref.putIdentifier(s2t('layer'), ID);
var d = new ActionDescriptor();
d.putReference(s2t('null'), ref);
executeAction(s2t('delete'), d);
}
function getDescValue(descriptor, property) {
switch (descriptor.getType(property)) {
case DescValueType.OBJECTTYPE: return { type: t2s(descriptor.getObjectType(property)), value: descriptor.getObjectValue(property) };
case DescValueType.LISTTYPE: return descriptor.getList(property);
case DescValueType.REFERENCETYPE: return descriptor.getReference(property);
case DescValueType.BOOLEANTYPE: return descriptor.getBoolean(property);
case DescValueType.STRINGTYPE: return descriptor.getString(property);
case DescValueType.INTEGERTYPE: return descriptor.getInteger(property);
case DescValueType.LARGEINTEGERTYPE: return descriptor.getLargeInteger(property);
case DescValueType.DOUBLETYPE: return descriptor.getDouble(property);
case DescValueType.ALIASTYPE: return descriptor.getPath(property);
case DescValueType.CLASSTYPE: return descriptor.getClass(property);
case DescValueType.UNITDOUBLE: return (descriptor.getUnitDoubleValue(property));
case DescValueType.ENUMERATEDTYPE: return { type: t2s(descriptor.getEnumerationType(property)), value: t2s(descriptor.getEnumerationValue(property)) };
default: return 0;
};
}
Copy link to clipboard
Copied
Edit - I still don't know how to edit a comment, so here it is
it's not as fast as I expected - takes like 10-20 seconds on large files, but that will have to do for now.
Copy link to clipboard
Copied
Why don't you use deletion method from my posted snippet to accelerate your script?
Copy link to clipboard
Copied
tbh, I don't understand the code you sent, so I couldn't integrate it even if I wanted to.
Copy link to clipboard
Copied
Have you tried this code? You need only deletion function part that works the way you put to it all layerSets id's to delete them all at once, not singly like you do it.
Copy link to clipboard
Copied
so I can just use loads of putIdentifier into the one action reference, and then do a single delete?
that should speed it up by a LOT. I'll try it right away
Question, is there a way to edit a comment? So I could make this change if it's that much faster?
Copy link to clipboard
Copied
If that is possible for regular users then maybe after some amount of posts.
Post edited part of code so I'm going to put in to there and then 'delete' the latest post.