Skip to main content
Participating Frequently
November 21, 2021
Answered

ExtendScript in Photoshop: traversing layer list faster

  • November 21, 2021
  • 1 reply
  • 2863 views

Hi guys,

 

I'm working on a complicated script that makes use of layer/group hierarchy and directives in layer names. To get the process started, the first thing the script does is recursively traverse the whole layer tree to collect the data and create js objects with all the necessary information I need to proceed.

 

Right now I have a recursive function that takes a layer set, and then basically does this:

 

function AnalyzeGroup(group) {
  for (var i = 0; i < group.layers.length; i++) {
    var layer = group.layers[i];
    // ...work with layer...
    if (layer.typename == 'LayerSet')
      AnalyzeGroup(layer);
  }
}

 

My problem is, this process is ridiculously, unbelievably slow. The delay can already be felt at dozens of layers, but some of the documents I need this to run on contain hundreds or even thousands of layers, and the delay grows fast. Traversing an average document might take 5 seconds, but today I was brave enough to run the script on a document with 2000+ layers, and the initial stage took literally 15 minutes.

 

I understand that this is probably not a "normal" usecase, and I suspect nothing can be done to improve this, but taking 15 minutes to trivially traverse a thousand items doesn't feel right to me, so maybe I'm missing something essential here? Idk.

 

As I understand it, simply accessing document elements via this API is very costly for some reason. If I could somehow, say, instantly receive a quickly accessible copy of the whole hierarchy to process, it would be fantastic. Although I do need full info on hierarchy, layer names and visibility... and, well, some sort of reference to the layer objects themselves, as I use them later during the actual export.

 

I don't have insanely high hopes for solving this, but any advice will be greatly appreciated. Thanks for hearing me out!

 

This topic has been closed for replies.
Correct answer jazz-y

The getLayersCollection () function returns an array of objects with a structure similar to what you see in the DOM:

 

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 layerSection = lr.getProperty('layerSection', i, true).value
            if (layerSection == '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.type = lr.getProperty('layerKind', i, true)
            properties.visible = lr.getProperty('visible', i, true)

            if (layerSection == 'layerSectionStart') {
                for (o in properties) { parentItem[o] = properties[o] }
                group.push(parentItem);
                return i;
            } else {
                parentItem.push(properties)
            }
        }
        return parentItem
    }
}

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
    }

    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;
        };
    }
}

 

 

The speed of the script depends, among other things, on how many properties you plan to collect. The code above collects the following data:

 

properties.name = lr.getProperty('name', i, true)
properties.id = lr.getProperty('layerID', i, true)
properties.type = lr.getProperty('layerKind', i, true)
properties.visible = lr.getProperty('visible', i, true)

 

It takes 350 milliseconds to read data from 2000 layers on my computer.

 

(in a similar way, you can add the properties you need to properties object yourself - you can see how to get the names of the available properties in the topic  Action Manager Scripting)

1 reply

jazz-yCorrect answer
Legend
November 21, 2021

The getLayersCollection () function returns an array of objects with a structure similar to what you see in the DOM:

 

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 layerSection = lr.getProperty('layerSection', i, true).value
            if (layerSection == '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.type = lr.getProperty('layerKind', i, true)
            properties.visible = lr.getProperty('visible', i, true)

            if (layerSection == 'layerSectionStart') {
                for (o in properties) { parentItem[o] = properties[o] }
                group.push(parentItem);
                return i;
            } else {
                parentItem.push(properties)
            }
        }
        return parentItem
    }
}

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
    }

    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;
        };
    }
}

 

 

The speed of the script depends, among other things, on how many properties you plan to collect. The code above collects the following data:

 

properties.name = lr.getProperty('name', i, true)
properties.id = lr.getProperty('layerID', i, true)
properties.type = lr.getProperty('layerKind', i, true)
properties.visible = lr.getProperty('visible', i, true)

 

It takes 350 milliseconds to read data from 2000 layers on my computer.

 

(in a similar way, you can add the properties you need to properties object yourself - you can see how to get the names of the available properties in the topic  Action Manager Scripting)

Participating Frequently
November 24, 2021

Thanks forthe answer! I fiddled around with it for a while, but I couldn't quite get everything I needed out of it — the hierarchy was a bit confusing with the way it represented groups, and on top of that I couldn't figure out how to get layer bounds (I forgot to mention in the post that I needed that data). So I began studying the link you provided.

 

While I was experimenting with all that and researching other issues, I stumbled upon this post. @r-bin in their answer there included code for the function get_layer_by_id(). That function completely fails to actually yield layer by ID for me for some reason, but instead in the very beginning it retreives exhaustive data on a document in the form of a single giant js object. That thing has the whole layer structure, every possible property of every layer (including bounds, clipping, blending, styles, etc.), properties of the document including a simple list of selected layer indices as a bonus, it's magical. Thanks to all those rich easily accessible properties I'm starting to get ideas for new features for my script I previously wouldn't dare to dream about including.

So I modified the function to get exactly what I needed, and I ended up with this:

function getDocData() {
    var r = new ActionReference();
    r.putProperty(charIDToTypeID("Prpr"), stringIDToTypeID("json"));
    r.putEnumerated(charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
    
    eval("var json = " + executeActionGet(r).getString(stringIDToTypeID("json")));

    return json;
}

 

Here's an example of an object returned by this function:

{
   "version":"1.6.1",
   "timeStamp":1637666442.008,
   "count":14,
   "id":3302,
   "file":".../laboratory.psd",
   "bounds":{
      "top":0,
      "left":0,
      "bottom":392,
      "right":640
   },
   "selection":[
      3165
   ],
   "resolution":72,
   "globalLight":{
      "angle":30,
      "altitude":30
   },
   "generatorSettings":false,
   "profile":"sRGB IEC61966-2.1",
   "mode":"RGBColor",
   "depth":8,
   "layers":[
      {
         "id":10234,
         "index":4403,
         "type":"layerSection",
         "name":"pipeline_top + normal",
         "bounds":{
            "top":0,
            "left":0,
            "bottom":0,
            "right":0
         },
         "visible":false,
         "clipped":false,
         "blendOptions":{
            "mode":"passThrough"
         },
         "generatorSettings":false,
         "layers":[
            {
               "id":7447,
               "index":4402,
               "type":"layer",
               "name":"pipeline_top_4",
               "bounds":{
                  "top":0,
                  "left":208,
                  "bottom":88,
                  "right":360
               },
               "visible":false,
               "clipped":false,
               "blendOptions":{
                  "opacity":{
                     "value":49,
                     "units":"percentUnit"
                  }
               },
               "pixels":true,
               "generatorSettings":false
            },
...

This works really fast with anything I throw at it. Combined with the function that hides/shows layers by ID list (the very one you gave me in your answer to my previous post!) this solves almost everything and opens a lot of doors.

Thank you again!

Kukurykus
Legend
November 24, 2021

Towards to your original question requirements, jazz-y solution is correct.