• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
0

ExtendScript in Photoshop: traversing layer list faster

Explorer ,
Nov 21, 2021 Nov 21, 2021

Copy link to clipboard

Copied

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!

 

TOPICS
Actions and scripting

Views

1.5K

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct answer

Guide , Nov 21, 2021 Nov 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 : [];
  
...

Votes

Translate

Translate
Adobe
Guide ,
Nov 21, 2021 Nov 21, 2021

Copy link to clipboard

Copied

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)

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Nov 23, 2021 Nov 23, 2021

Copy link to clipboard

Copied

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!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
LEGEND ,
Nov 24, 2021 Nov 24, 2021

Copy link to clipboard

Copied

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

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Nov 24, 2021 Nov 24, 2021

Copy link to clipboard

Copied

If I was a user who found this post while having the same problem, I would've preferred the answer I've marked as the solution because it's way simpler (effectively just 4 lines of code) and the results are just cleaner and more complete. Not to diminish jazz-y's effort, but his version is overly complicated compared to the snippet I found and posted, and it didn't quite yield an object with full hierarchy of layers like I requested, the group layers themselves were absent from the data. So, technically speaking, no, it wasn't. I didn't pass on it only because of the bounds thing.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Nov 24, 2021 Nov 24, 2021

Copy link to clipboard

Copied

Different solutions to the same problem are always good 🙂

 

How did you check the array returned by the getLayersCollection () function? Have you noticed that each element of the array is also an object? It contains information about the groups (here, perhaps, I should have given explanations in advance):

2021-11-24_19-59-09.png

2021-11-24_20-03-40.png

Initially, you did not say how much information you need to collect about the layers - my code allows you to manually specify any desired attribute, i.e. it can be easily adapted to any task.

 

It's good that you have enough information that is stored in the json property of the document, but for someone this information may not be enough.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Nov 24, 2021 Nov 24, 2021

Copy link to clipboard

Copied

quote

How did you check the array returned by the getLayersCollection () function? Have you noticed that each element of the array is an object? It contains information about the groups


I did get the json object in stringified form and explore it, but apparently I somehow missed the fact group layers are contained in the end of each group array... I expected the hierarchy to be represented more conventionally with some form of separate named array inside a group layer object (i.e. group.layers[i]), so I started rewriting the code to yield this result and probably broke something along the way. Taking it back then, not your code's fault after all.

quote

Initially, you did not say how much information you need to collect about the layers


I actually did:
...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 realized just now that the reason I didn't request bounds as part of the data I needed the result to contain is because I'd hoped to use the requested layer references to obtain the bounds. I meant references to the DOM ArtLayer objects (maybe that part wasn't clear from the way I wrote it), but your code didn't provide those (not sure how it even could, but hey, technically both our solutions are not complete then I guess!), so I tried collecting the bounds using the link you provided. And it worked, but the bounds data retrieved that way wasn't usable, so I assumed I needed to drill deeper, and it became increasingly cryptic and puzzle-ish. The search from there somehow lead me to the document json which just gave me everything I needed in 4 lines of code, so I came back to report.

All and any of this is much more than I initially hoped for, and with the knowledge I've collected so far even just from from your replies here and in other topics, Photoshop scripting almost feels like the comprehensive and powerful platform I initially imagined it to be before facing the AM and SL nightmare reality as a beginner. Didn't expect this community to be so helpful and insightful, thank you for being a huge part of it 🙂

Can't wait to port it all to UXP though 😄

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Nov 24, 2021 Nov 24, 2021

Copy link to clipboard

Copied

Yes, getting bounds requires some understanding of AM code, so I gave a link to another topic.

 

The following types of bounds can be obtained in AM:

  • bounds
  • boundsNoEffects
  • boundsNoMask

 

Since all these objects are ActionDescriptor, they need to be converted into an object:

 

properties.bounds = lr.descToObject(lr.getProperty('bounds', i, true).value)
properties.boundsNoEffects = lr.descToObject(lr.getProperty('boundsNoEffects', i, true).value)
properties.boundsNoMask = lr.descToObject(lr.getProperty('boundsNoMask', i, true).value)

 

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Nov 24, 2021 Nov 24, 2021

Copy link to clipboard

Copied

LATEST

Ah. That definitely makes it clearer, gracias!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines