Skip to main content
Known Participant
July 29, 2025
Answered

Is it possible to use scripting to align layers to layers with the same outline?

  • July 29, 2025
  • 2 replies
  • 5485 views

Hello everyone, I want to use a script to align all colored layers to gray layers according to their shape outlines, and some layers are rotated 180°. Can this function be achieved through a script? I'm not very familiar with scripts. I used ChatGPT to write the scripts I need, but this time, I don't know if this function can be achieved through script logic.

Correct answer jazz-y

首先非常感谢您的回复。收到您的回复后,我看了看我的文件,感觉自己像个傻子。在我给您的文件中,灰色图层和组的大小不一样(一个大,一个小)。我不敢相信我做了这么蠢的事。您的帮助让我完美地实现了我的需求,非常感谢!我也为我的愚蠢道歉,真的非常抱歉![我附上了一个更正后的文件。没有冒犯的意思。您的帮助代码帮助我实现了所需的操作要求。我只是觉得您开发了一些东西,所以希望您能看到正确的文件。] PS:如果我想加快代码速度,应该怎么做?


Your files have large linear dimensions. The most time-consuming operations are the selection expand operation (used when dividing a gray layer into fragments) and layer transformation operations (used to determine the optimal position of a figure and at the final stage of alignment). It is impossible to get rid of them. Changing the remaining commands will have an insignificant effect on the overall performance.

 

I added the constant SCALE = 0.25 to the code - this is the scale value by which the document is temporarily reduced in the process of calculating the parameters necessary for moving layers, that is, 25% of the original (in step 3, the document returns to its original state, and all coefficients are divided by the scale value). You can change this value, the smaller it is, the faster the script works (but the positioning accuracy is lower due to rounding errors), the larger it is, the higher the accuracy (1 is 100%, there is no point in setting it higher than one)

Code updated 2025-08-14 (3)

#target photoshop
const EXPAND_BY = 20, // expand selection from the opacity mask to minimize errors when separating objects
    WXHAccuracy = 100, // multiplier for converting WxH to integer (affects rounding accuracy)
    WXHTolerance = 0.15, // deviation from the proportion value, below which objects are still compared
    SCALE = 0.25;
var apl = new AM('application'),
    doc = new AM('document'),
    lr = new AM('layer'),
    pth = new AM('path');
if (apl.getProperty('numberOfDocuments')) {
    var hst = activeDocument.activeHistoryState,
        layers = [], deltas = [];
    doc.setGlobalFxState(false);
    try {
        activeDocument.suspendHistory('Checking layers', 'main(1)');
        if (layers.frames.length == layers.objects.length) activeDocument.suspendHistory('Find optimal position', 'main(2)');
        activeDocument.activeHistoryState = hst;
        activeDocument.suspendHistory('Align layers', 'main(3)');
    } catch (e) { activeDocument.activeHistoryState = hst; alert(e) }
    doc.setGlobalFxState(true);
    function main(step) {
        switch (step) {
            case 1: doForcedProgress('Step 1/3: Get initial bounds of layers', 'step1()'); break;
            case 2: doForcedProgress('Step 2/3: Find optimal position for layers', 'step2()'); break;
            case 3: doForcedProgress('Step 3/3: Align layers', 'step3()'); break;
        }
        function step1() {
            layers = findLayers();
            doc.setScale(SCALE * 100);
            if (layers.frames.length) {
                changeProgressText('Step 1/3: Preparing to split gray shapes')
                layers.frames = grayFramesToLayers(layers.frames);
                if (layers.frames.length == layers.objects.length) {
                    var docRes = doc.getProperty('resolution');
                    for (var i = 0; i < layers.objects.length; i++) {
                        lr.selectLayer(layers.objects[i].id)
                        layers.objects[i] = describeLayer(layers.objects[i].id, doc.getProperty('width') * docRes / 72, doc.getProperty('height') * docRes / 72)
                    }
                } else throw new Error('The number of gray shapes found does not match the number of objects found!')
            } else throw new Error('No shape layers found!')
        }
        function step2() {
            var result = alignLayers(layers.frames, layers.objects);
            for (var i = 0; i < result.length; i++) {
                changeProgressText('Step 2/3: Find optimal position for layers ' + Math.round(((i + 1) / result.length) * 100) + '%')
                deltas.push({ layer: result[i], transform: checkOrientation(result[i]) })
            };
        }
        function step3() {
            for (var i = 0; i < deltas.length; i++) {
                lr.selectLayer(deltas[i].layer.id)
                changeProgressText('Step 3/3: Align layer: ' + lr.getProperty('name', deltas[i].layer.id))
                lr.transform(deltas[i].transform, deltas[i].layer.center[0] / SCALE, deltas[i].layer.center[1] / SCALE)
                lr.move(deltas[i].layer.dX / SCALE, deltas[i].layer.dY / SCALE)
            };
        }
    }
}
function findLayers() {
    var layers = getLayersCollection(),
        result = {};
    result.frames = [];
    result.objects = [];
    for (a in layers) {
        changeProgressText('Step 1/3: Get initial bounds of layers ' + Math.round((parseInt(a) + 1) / (layers.length) * 100) + '%')
        if (layers[a].length) {
            lr.selectLayer(layers[a].id)
            lr.merge();
            var id = lr.getProperty('layerID');
            lr.setVisiblity(id, 'hide')
            result.objects.push({ id: id })
        } else result.frames.push({ id: layers[a].id })
    }
    return result;
}
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);
            if (layerSection == 'layerSectionEnd') {
                i = layersCollection(i + 1, to, [], parentItem)
                continue;
            }
            if (lr.getProperty('background', i, true)) continue;
            var properties = {};
            properties.id = lr.getProperty('layerID', 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 alignLayers(a, b) {
    var result = [];
    do {
        var cur = b.shift(),
            target = findNearest(cur, a);
        lr.selectLayer(cur.id);
        lr.move((target.center[0] - cur.center[0]) - cur.offsetX, (target.center[1] - cur.center[1]) - cur.offsetY)
        result.push({ id: cur.id, bounds: target.bounds, dX: target.center[0] - cur.center[0], dY: target.center[1] - cur.center[1], center: cur.center, targetId: target.id })
    } while (b.length)
    return result;
}
function findNearest(a, b) {
    var dist = [];
    for (var i = 0; i < b.length; i++) {
        if (b[i].found) continue;
        if (Math.abs(b[i].WxH - a.WxH) > WXHTolerance) continue;
        dist.push({ i: i, dist: Math.abs(b[i].square - a.square) });
    }
    if (!dist.length) throw new Error('Pair for layer ' + lr.getProperty('name', a.id) + ' not found!');
    dist.sort(function (a, b) { return a.dist > b.dist ? 1 : -1 })
    b[dist[0].i].found = true;
    return b[dist[0].i];
}
function checkOrientation(o) {
    var transformMatrices = [[100, -100], [-100, 100], [100, -100], [-100, 100]],
        transform = [[100, -100], [-100, -100], [-100, 100], [100, 100]],
        result = [];
    lr.selectLayer(o.id, true)
    lr.setBlendingMode('blendSubtraction');
    for (var i = 0; i < transformMatrices.length; i++) result.push(findDifference(o, transformMatrices[i], transform[i]));
    lr.delete(o.id);
    lr.delete(o.targetId);
    result.sort(function (a, b) { return a[0] > b[0] ? 1 : -1 })
    return result[0][1]
    function findDifference(o, transformMatrices, transform) {
        lr.removeSelection()
        lr.transform(transformMatrices)
        lr.selectLayer(o.targetId)
        lr.selectTransparency()
        var c = getAverageColor(doc.getProperty('histogram'))
        lr.selectLayer(o.id, true)
        return [c, transform]
    }
}
function grayFramesToLayers(l) {
    var result = [];
    for (var a in l) isolateLayers(l[a].id, lr.getProperty('name', l[a].id), result)
    return result;
    function isolateLayers(id, title, result) {
        lr.selectLayer(id)
        lr.selectTransparency();
        lr.expandSelection(EXPAND_BY * SCALE);
        pth.workPathFromSelection(1);
        var pathComponents = (pth.getProperty('pathContents')).getList(stringIDToTypeID('pathComponents'));
        if (pathComponents.count > 1) {
            for (var i = pathComponents.count - 1; i >= 0; i--) {
                changeProgressText('Step 1/3: Split gray shapes to layers ' + Math.round((pathComponents.count - 1 - i) / (pathComponents.count - 1) * 100) + '%')
                pth.workPathFromDesc(pathComponents.getObjectValue(i));
                pth.selectionFromWorkPath();
                lr.selectLayer(id)
                if (lr.layerViaCut()) { result.push(describeLayer(lr.getProperty('layerID'))) }
            }
            pth.delete();
        } else { result.push(describeLayer(id)) }
    }
}
function describeLayer(id, docW, docH) {
    var o = {},
        bounds = lr.descToObject(lr.getProperty('boundsNoEffects', id));
    o.id = id;
    o.width = bounds.right - bounds.left
    o.heigth = bounds.bottom - bounds.top
    o.bounds = bounds
    o.center = [bounds.left + o.width / 2, bounds.top + o.heigth / 2]
    o.found = false;
    if (docW && docH) {
        o.offsetX = o.bounds.left < 0 ? 0 - (o.bounds.left) : (o.bounds.right > docW ? docW - (o.bounds.right) : 0);
        o.offsetY = o.bounds.top < 0 ? 0 - (o.bounds.top) : (o.bounds.bottom > docH ? docH - (o.bounds.bottom) : 0);
        if (o.offsetX != 0 || o.offsetY != 0) lr.move(o.offsetX, o.offsetY, 0, 0)
    }
    lr.selectTransparency()
    o.square = countPixels(doc.getProperty('histogram'))
    o.WxH = Math.round((o.width / o.heigth) * WXHAccuracy) / WXHAccuracy
    return o
}
function getAverageColor(h) {
    var n = p = 0;
    for (var i = 0; i < h.count; i++) {
        n += h.getInteger(i)
        p += h.getInteger(i) * i
    }
    return p / n
}
function countPixels(h) {
    var s = 0;
    for (var i = 0; i < h.count; i++) {
        s += h.getInteger(i)
    }
    return s
}
function AM(target) {
    var s2t = stringIDToTypeID,
        t2s = typeIDToStringID;
    target = s2t(target)
    this.getProperty = 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'), 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'), 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.selectLayer = function (id, mode) {
        (r = new ActionReference()).putIdentifier(s2t('layer'), id);
        (d = new ActionDescriptor()).putReference(s2t('null'), r);
        d.putBoolean(s2t('makeVisible'), mode ? mode : false)
        executeAction(s2t('select'), d, DialogModes.NO);
    }
    this.selectTransparency = function () {
        (r = new ActionReference()).putProperty(s2t('channel'), s2t('selection'));
        (d = new ActionDescriptor()).putReference(s2t('null'), r);
        r1 = new ActionReference();
        r1.putEnumerated(s2t('channel'), s2t('channel'), s2t('transparencyEnum'));
        d.putReference(s2t('to'), r1);
        executeAction(s2t('set'), d, DialogModes.NO);
    }
    this.layerViaCut = function () {
        try {
            executeAction(s2t("copyToLayer"), d, DialogModes.NO);
        } catch (e) { return false }
        return true;
    }
    this.expandSelection = function (pixels) {
        (d = new ActionDescriptor()).putUnitDouble(s2t("by"), s2t("pixelsUnit"), pixels);
        d.putBoolean(s2t("selectionModifyEffectAtCanvasBounds"), false);
        executeAction(s2t("expand"), d, DialogModes.NO);
    }
    this.removeSelection = function () {
        (r = new ActionReference()).putProperty(s2t('channel'), s2t('selection'));
        (d = new ActionDescriptor()).putReference(s2t('null'), r);
        d.putEnumerated(s2t('to'), s2t('ordinal'), s2t('none'));
        executeAction(s2t('set'), d, DialogModes.NO);
    }
    this.workPathFromSelection = function (tolerance) {
        (r = new ActionReference()).putClass(s2t("path"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        (r1 = new ActionReference()).putProperty(s2t("selectionClass"), s2t("selection"));
        d.putReference(s2t("from"), r1);
        d.putUnitDouble(s2t("tolerance"), s2t("pixelsUnit"), tolerance);
        executeAction(s2t("make"), d, DialogModes.NO);
    }
    this.selectionFromWorkPath = function () {
        (r = new ActionReference()).putProperty(s2t("channel"), s2t("selection"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        (r1 = new ActionReference()).putProperty(s2t("path"), s2t("workPath"));
        d.putReference(s2t("to"), r1);
        executeAction(s2t("set"), d, DialogModes.NO);
    }
    this.workPathFromDesc = function (desc) {
        (r = new ActionReference()).putProperty(s2t('path'), s2t('workPath'));
        (d = new ActionDescriptor()).putReference(s2t('target'), r);
        (l = new ActionList).putObject(s2t('pathComponent'), desc);
        d.putList(s2t('to'), l);
        executeAction(s2t('set'), d, DialogModes.NO);
    }
    this.merge = function () {
        executeAction(s2t("mergeLayers"), new ActionDescriptor(), DialogModes.NO);
    }
    this.delete = function (id) {
        var r = new ActionReference();
        if (id) r.putIdentifier(target, id) else r.putEnumerated(target, s2t('ordinal'), s2t('targetEnum'));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        executeAction(s2t("delete"), d, DialogModes.NO);
    }
    this.move = function (dX, dY) {
        (r = new ActionReference()).putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        (d1 = new ActionDescriptor()).putUnitDouble(s2t("horizontal"), s2t("pixelsUnit"), dX);
        d1.putUnitDouble(s2t("vertical"), s2t("pixelsUnit"), dY);
        d.putObject(s2t("to"), s2t("offset"), d1);
        executeAction(s2t("move"), d, DialogModes.NO);
    }
    this.setBlendingMode = function (mode) {
        (r = new ActionReference()).putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        (d1 = new ActionDescriptor()).putEnumerated(s2t("mode"), s2t("blendMode"), s2t(mode));
        d.putObject(s2t("to"), s2t("layer"), d1);
        executeAction(s2t("set"), d, DialogModes.NO);
    }
    this.transform = function (transform, dX, dY) {
        (r = new ActionReference()).putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        if (dX != undefined) {
            d.putEnumerated(s2t("freeTransformCenterState"), s2t("quadCenterState"), s2t("QCSIndependent"));
            (d1 = new ActionDescriptor()).putUnitDouble(s2t("horizontal"), s2t("pixelsUnit"), dX);
            d1.putUnitDouble(s2t("vertical"), s2t("pixelsUnit"), dY);
            d.putObject(s2t("position"), charIDToTypeID("Pnt "), d1);
            (d2 = new ActionDescriptor()).putUnitDouble(s2t("horizontal"), s2t("pixelsUnit"), 0);
            d2.putUnitDouble(s2t("vertical"), s2t("pixelsUnit"), 0);
            d.putObject(s2t("offset"), s2t("offset"), d2);
        } else {
            d.putEnumerated(s2t("freeTransformCenterState"), s2t("quadCenterState"), s2t("QCSAverage"));
        }
        d.putUnitDouble(s2t("width"), s2t("percentUnit"), transform[0]);
        d.putUnitDouble(s2t("height"), s2t("percentUnit"), transform[1]);
        d.putBoolean(s2t("linked"), true);
        d.putEnumerated(s2t("interpolation"), s2t("interpolationType"), s2t("bicubic"));
        executeAction(s2t("transform"), d, DialogModes.NO);
    }
    this.setScale = function (width) {
        (d = new ActionDescriptor()).putUnitDouble(s2t("width"), s2t("percentUnit"), width);
        d.putBoolean(s2t("scaleStyles"), true);
        d.putBoolean(s2t("constrainProportions"), true);
        d.putEnumerated(s2t("interpolation"), s2t("interpolationType"), s2t("bicubicSharper"));
        executeAction(s2t("imageSize"), d, DialogModes.NO);
    }
    this.setGlobalFxState = function (layerFXVisible) {
        (r = new ActionReference()).putProperty(s2t("property"), s2t("layerFXVisible"));
        r.putEnumerated(s2t("document"), s2t("ordinal"), s2t("targetEnum"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        (d1 = new ActionDescriptor()).putBoolean(s2t("layerFXVisible"), layerFXVisible);
        d.putObject(s2t("to"), s2t("layerFXVisible"), d1);
        executeAction(s2t("set"), d, DialogModes.NO);
    }
    this.setVisiblity = function (id, mode) {
        (r = new ActionReference()).putIdentifier(s2t('layer'), id);
        (d = new ActionDescriptor()).putReference(s2t('target'), r);
        executeAction(s2t(mode), d, DialogModes.NO);
    }
    function getDescValue(d, p) {
        switch (d.getType(p)) {
            case DescValueType.OBJECTTYPE: return (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 t2s(d.getEnumerationValue(p));
            default: break;
        };
    }
}

2 replies

Inspiring
August 13, 2025

What is cruser?

Legend
July 29, 2025

Photoshop does not perceive the shapes on the merged layer (03) as separate objects. Yes, they can be separated by a script using a transparency mask, but then the question arises of how to find a colored object corresponding to each gray shape:

  • If the colored objects are always located above the corresponding shape - yes, this can be done (and not necessarily with a script, you can simply use an action using the move tool and the align command).
  • If the colored shapes can differ in size from the gray shapes - only with a script.
  • If the position of the colored shapes changes, then user participation is required (gray shapes must be divided into layers in advance, the user manually selects two layers - one colored and one gray, the script or action combines them)

The situation with flipping the image by 180 degrees already relates to image recognition, Photoshop currently does not provide tools for effective recognition and classification of objects. In theory, the problem can be solved algorithmically, but it is unlikely that false positives will be eliminated if the objects are similar.

 

Perhaps you should describe the problem in more detail. Do the shapes of the objects change? Are the colored objects always located above the gray ones and match them in color? Is user participation acceptable for manual matching of objects (for example, by selecting two layers in the layers palette) and other points that will help us find a solution.

Known Participant
August 7, 2025

Thanks for your reply! The color layer can ensure that it is always on top of the gray layer, but the color I can only ensure that the gray layer is always gray. The color layer will be various colors, and I often need to carry out this step with files of various different shapes of layers. This is why I need a script to complete it, and I hope to use a script to complete it. If manual matching is required, this will be very cumbersome.

Inspiring
August 7, 2025

I write Photoshop scripts for living and what you are trying to do is just not possible. The only "align" tool available in Photoshop is "Auto-Align Layers" and for that to work you'd need, at least temporarly, seperate all shapes and give them backgroud so the layers are not transparent, but even after all that the align bit is not accurate: