Skip to main content
Known Participant
April 3, 2020
Question

Delete empty masks - Layer Masks, Vector Masks, and Filter Masks

  • April 3, 2020
  • 6 replies
  • 2603 views

I'm looking for a way to detect and delete empty Layer Masks and Filter Masks.

 

Layers and Layer Sets have two properties that make this easy to detect for Vector Masks.

Properties: 

hasVectorMask
vectorMaskEmpty
 

Is there a way to detect empty Layer Masks and empty Filter Masks?

If it's possible to get the bounds of Layer Masks and Filter Masks that could lead to a solution.

 

This works for Vector Masks:

var actionReference = new ActionReference();
actionReference.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
var actionDescriptor = executeActionGet(actionReference);
var hasVectorMask = actionDescriptor.getBoolean(stringIDToTypeID("hasVectorMask"));
var vectorMaskEmpty = actionDescriptor.getBoolean(stringIDToTypeID("vectorMaskEmpty"));

if (hasVectorMask && vectorMaskEmpty) {
    var iddelete = stringIDToTypeID("delete");
    var desc8 = new ActionDescriptor();
    var idnull = stringIDToTypeID("null");
    var ref7 = new ActionReference();
    var idpath = stringIDToTypeID("path");
    var idpath = stringIDToTypeID("path");
    var idvectorMask = stringIDToTypeID("vectorMask");
    ref7.putEnumerated(idpath, idpath, idvectorMask);
    var idlayer = stringIDToTypeID("layer");
    var idordinal = stringIDToTypeID("ordinal");
    var idtargetEnum = stringIDToTypeID("targetEnum");
    ref7.putEnumerated(idlayer, idordinal, idtargetEnum);
    desc8.putReference(idnull, ref7);
    executeAction(iddelete, desc8, DialogModes.NO);
}

 

This topic has been closed for replies.

6 replies

Stephen Marsh
Community Expert
Community Expert
April 7, 2020

Ah, the beauty of serendipity...

 

https://github.com/ES-Collection/Photoshop-Scripts/blob/master/Link%20All%20Masks.jsx

 

Look for the following functions:

 

hasVectorMask

hasLayerMask

hasFilterMask

mikebaughAuthor
Known Participant
April 5, 2020

I ended up going with r-bin's solution for both Layer Masks and Filter Masks since the histogram test wasn't working for Filter Masks. I optimized a bit and it runs faster. If you see anything that could be optimized further let me know.

 

Here is the working version of the code that deletes all empty layer masks, empty vector masks, and empty filter masks. I hope someone finds this useful.

 

// Variables
var app,
    documents,
    ActionDescriptor,
    ActionReference,
    executeAction,
    executeActionGet,
    DialogModes,
    stringIDToTypeID,
    documentStats = {};

// Delete Empty Masks
function deleteEmptyMasks(layer) {

    // Variables
    var layerReference,
        layerProperties,
        hasUserMask,
        userMaskReference,
        userMaskEmpty,
        hasVectorMask,
        vectorMaskReference,
        vectorMaskEmpty,
        hasFilterMask,
        filterMaskReference,
        filterMaskEmpty,
        selectionReference,
        selectionDescriptor,
        deleteDescriptor;

    // Get Layer Properties
    layerReference = new ActionReference();
    layerReference.putIdentifier(stringIDToTypeID("layer"), layer.id);
    layerProperties = executeActionGet(layerReference);
    hasUserMask = layerProperties.getBoolean(stringIDToTypeID("hasUserMask"));
    hasVectorMask = layerProperties.getBoolean(stringIDToTypeID("hasVectorMask"));
    vectorMaskEmpty = layerProperties.getBoolean(stringIDToTypeID("vectorMaskEmpty"));
    hasFilterMask = layerProperties.getBoolean(stringIDToTypeID("hasFilterMask"));

    // If No Masks Exist Return
    if (!hasUserMask && !hasVectorMask && !hasFilterMask) {
        return;
    }

    // Setup Selection For User Mask And Filter Mask Check
    if (hasUserMask || hasFilterMask) {
        selectionReference = new ActionReference();
        selectionDescriptor = new ActionDescriptor();
        selectionReference.putProperty(stringIDToTypeID("channel"), stringIDToTypeID("selection"));
        selectionDescriptor.putReference(stringIDToTypeID("null"), selectionReference);
    }

    // If A User Mask Exist Test To See If Empty
    if (hasUserMask) {
        userMaskReference = new ActionReference();
        userMaskReference.putEnumerated(stringIDToTypeID("channel"), stringIDToTypeID("channel"), stringIDToTypeID("mask"));
        userMaskReference.putIdentifier(stringIDToTypeID("layer"), layer.id);
        selectionDescriptor.putReference(stringIDToTypeID("to"), userMaskReference);
        executeAction(stringIDToTypeID("set"), selectionDescriptor, DialogModes.NO);
        userMaskEmpty = true;
        try {
            app.activeDocument.selection.bounds;
        } catch (error) {
            userMaskEmpty = false;
        }
        if (userMaskEmpty) {
            app.activeDocument.selection.invert();
            try {
                app.activeDocument.selection.bounds;
                userMaskEmpty = false;
            } catch (ignore) {
            }
            app.activeDocument.selection.deselect();
        }
    }

    // If Filter Mask Exist Test To See If Empty
    if (hasFilterMask) {
        filterMaskReference = new ActionReference();
        filterMaskReference.putEnumerated(stringIDToTypeID("channel"), stringIDToTypeID("channel"), stringIDToTypeID("filterMask"));
        filterMaskReference.putIdentifier(stringIDToTypeID("layer"), layer.id);
        selectionDescriptor.putReference(stringIDToTypeID("to"), filterMaskReference);
        executeAction(stringIDToTypeID("set"), selectionDescriptor, DialogModes.NO);
        filterMaskEmpty = true;
        try {
            app.activeDocument.selection.bounds;
        } catch (error) {
            filterMaskEmpty = false;
        }
        if (filterMaskEmpty) {
            app.activeDocument.selection.invert();
            try {
                app.activeDocument.selection.bounds;
                filterMaskEmpty = false;
            } catch (ignore) {
            }
            app.activeDocument.selection.deselect();
        }
    }

    // If Empty Masks Exist Setup Delete Action Descriptor Else Return
    if (userMaskEmpty || vectorMaskEmpty || filterMaskEmpty) {
        deleteDescriptor = new ActionDescriptor();
    } else {
        return;
    }

    // If User Mask Is Empty Delete It
    if (userMaskEmpty) {
        try {
            deleteDescriptor.putReference(stringIDToTypeID("null"), userMaskReference);
            executeAction(stringIDToTypeID("delete"), deleteDescriptor, DialogModes.NO);
            documentStats.emptyUserMasksCount += 1;
        } catch (ignore) {
        }
    }

    // If Vector Mask Is Empty Delete It
    if (vectorMaskEmpty) {
        try {
            vectorMaskReference = new ActionReference();
            vectorMaskReference.putEnumerated(stringIDToTypeID("path"), stringIDToTypeID("path"), stringIDToTypeID("vectorMask"));
            vectorMaskReference.putIdentifier(stringIDToTypeID("layer"), layer.id);
            deleteDescriptor.putReference(stringIDToTypeID("null"), vectorMaskReference);
            executeAction(stringIDToTypeID("delete"), deleteDescriptor, DialogModes.NO);
            documentStats.emptyVectorMasksCount += 1;
        } catch (ignore) {
        }
    }

    // If Filter Mask Is Empty Delete It
    if (filterMaskEmpty) {
        try {
            deleteDescriptor.putReference(stringIDToTypeID("null"), filterMaskReference);
            executeAction(stringIDToTypeID("delete"), deleteDescriptor, DialogModes.NO);
            documentStats.emptyFilterMasksCount += 1;
        } catch (ignore) {
        }
    }
}

// Recursive Function Handles All Layers
function layerHandler(layerSet) {
    var layerVisible,
        layer,
        i;

    for (i = 0; i < layerSet.layers.length; i += 1) {
        layer = layerSet.layers[i];
        layerVisible = layer.visible;
        deleteEmptyMasks(layer);
        layer.visible = layerVisible;
        if (layer.typename === "LayerSet") {
            layerHandler(layer);
        }
    }
}

// Main
function main() {
    if (documents.length === 0) {
        alert("There are no documents open.");
    } else {
        app.bringToFront();
        try {
            documentStats.emptyUserMasksCount = 0;
            documentStats.emptyVectorMasksCount = 0;
            documentStats.emptyFilterMasksCount = 0;
            app.activeDocument.suspendHistory("Delete Empty Masks", "layerHandler(app.activeDocument)");
            alert("Delete Empty Masks\n\nCOMPLETE\n\nDocument Statistics \nTotal empty layer masks deleted: " + documentStats.emptyUserMasksCount + "\nTotal empty vector masks deleted: " + documentStats.emptyVectorMasksCount + "\nTotal empty filter masks deleted: " + documentStats.emptyFilterMasksCount);
        } catch (error) {
            alert("Select a layer and try again");
        }
    }
}

main();

 

Stephen Marsh
Community Expert
Community Expert
April 5, 2020

Good job! It appears to work as expected, I tried to break it but I couldn't...

 

So, what else are you looking to clean up?

Stephen Marsh
Community Expert
Community Expert
April 4, 2020

Thank you for your posts Mike-Baugh – as a scripting beginner, this is above my experience and knowledge. As you have not had replies from the other regular intermediate/advanced scripting contributors I'm guessing that there is nothing to add, otherwise they would have done so. I'm looking forward to seeing the final code... Is this part of a larger project or a stand-alone script to just remove unused masks?

mikebaughAuthor
Known Participant
April 5, 2020

Hi Stephen, Thanks for the response. I'm a scripting beginner too. There are a lot of great people on this forum who have shared great examples that have allowed me to get this far. I'm working on a collection of Photoshop scripts to help with my production workflow. This is one of them.

mikebaughAuthor
Known Participant
April 4, 2020

A solution from another thread for empty filter masks: Solution from r-bin 

I will implement this code and post full script when finished.

 

mikebaughAuthor
Known Participant
April 4, 2020

I haven't been able to detect empty filter masks yet.

 

Code to delete empty layer masks and empty vector masks below. It's working. Speed could be better.

 

// Variables
var app,
    documents,
    ActionDescriptor,
    ActionReference,
    executeAction,
    executeActionGet,
    DialogModes,
    stringIDToTypeID,
    totalPixels,
    documentStats = {};

// Delete Empty Layer Mask
function deleteEmptyLayerMask(layer) {
    try {
        app.activeDocument.activeLayer = layer;
        var actionReference = new ActionReference();
        actionReference.putEnumerated(stringIDToTypeID("channel"), stringIDToTypeID("channel"), stringIDToTypeID("mask"));
        var layerMaskProperties = executeActionGet(actionReference);
        var histogramArray = layerMaskProperties.getList(stringIDToTypeID("histogram"));
        var whiteValue = histogramArray.getInteger(255);
        if (whiteValue === totalPixels) {
            var actionDescriptor = new ActionDescriptor();
            actionDescriptor.putReference(stringIDToTypeID("null"), actionReference);
            executeAction(stringIDToTypeID("delete"), actionDescriptor, DialogModes.NO);
            documentStats.emptyLayerMasksCount += 1;
        }
    } catch (ignore) {
    }
}

// Delete Empty Vector Mask
function deleteEmptyVectorMask(layer) {
    try {
        var actionReference = new ActionReference();
        actionReference.putIdentifier(stringIDToTypeID("layer"), layer.id);
        var actionDescriptor = executeActionGet(actionReference);
        var hasVectorMask = actionDescriptor.getBoolean(stringIDToTypeID("hasVectorMask"));
        var vectorMaskEmpty = actionDescriptor.getBoolean(stringIDToTypeID("vectorMaskEmpty"));

        if (hasVectorMask && vectorMaskEmpty) {
            var actionDescriptorDelete = new ActionDescriptor();
            var actionReferenceDelete = new ActionReference();
            actionReferenceDelete.putEnumerated(stringIDToTypeID("path"), stringIDToTypeID("path"), stringIDToTypeID("vectorMask"));
            actionReferenceDelete.putIdentifier(stringIDToTypeID("layer"), layer.id);
            actionDescriptorDelete.putReference(stringIDToTypeID("null"), actionReferenceDelete);
            executeAction(stringIDToTypeID("delete"), actionDescriptorDelete, DialogModes.NO);
            documentStats.emptyVectorMasksCount += 1;
        }
    } catch (ignore) {
    }
}

// Recursive Function Handles All Layers
function deleteEmptyLayerMasks(layerSet) {
    var layerVisible,
        layer,
        i;

    for (i = 0; i < layerSet.layers.length; i += 1) {
        layer = layerSet.layers[i];
        layerVisible = layer.visible;
        deleteEmptyLayerMask(layer);
        deleteEmptyVectorMask(layer);
        layer.visible = layerVisible;
        if (layer.typename === "LayerSet") {
            deleteEmptyLayerMasks(layer);
        }
    }
}

// Main
function main() {
    if (documents.length === 0) {
        alert("There are no documents open.");
    } else {
        app.bringToFront();
        try {
            documentStats.emptyLayerMasksCount = 0;
            documentStats.emptyVectorMasksCount = 0;
            totalPixels = parseInt(app.activeDocument.width.as("px") * app.activeDocument.height.as("px"), 10);
            app.activeDocument.suspendHistory("Delete Empty Layer Masks", "deleteEmptyLayerMasks(app.activeDocument)");
            alert("Delete Empty Layer Masks\n\nCOMPLETE\n\nDocument Statistics \nTotal empty layer masks deleted: " + documentStats.emptyLayerMasksCount + "\nTotal empty vector masks deleted: " + documentStats.emptyVectorMasksCount);
        } catch (error) {
            alert("Select a layer and try again");
        }
    }
}

main();

 

mikebaughAuthor
Known Participant
April 4, 2020

The technique I implemented to detect if a layer mask is empty is as follows:

 

Get the histogram of the layer mask.

The histogram is represented as an array of 256 values.

The last value in the array is pure white.

If the last value equals the total document pixel count then the mask is completely pure white and is empty.

 

I'm still trying to solve the filter mask issue. The histogram values for a filter mask are always 0 regardless of what is in the mask. It seems like a bug, but it's most likely a syntax issue on my end. My test code snippet for empty filter masks is above if anyone wants a challenge.

 

mikebaughAuthor
Known Participant
April 3, 2020

It looks like the bounds trick probably won't work. Masks are flat channels with no transparency. Short of looping through all the pixels in a mask to check if they are all #FFFFFF any ideas on how to detect if mask is empty?

mikebaughAuthor
Known Participant
April 3, 2020

Would it be possible to detect an all-white mask using the histogram property from the mask?

mikebaughAuthor
Known Participant
April 3, 2020

This seems to work. Is there a better way to do this?

 

var totalPixels = parseInt(app.activeDocument.width.as("px") * app.activeDocument.height.as("px"));
var actionReference = new ActionReference();
actionReference.putEnumerated(stringIDToTypeID("channel"), stringIDToTypeID("channel"), stringIDToTypeID("mask"));
var actionDescriptorMask = executeActionGet(actionReference);
var propertyValue = actionDescriptorMask.getList(stringIDToTypeID("histogram"));
var whiteValue = propertyValue.getInteger(255);
if (whiteValue == totalPixels) {
    alert("Mask is empty");
} else {
    alert("Mask is not empty");
}