#target photoshop /* UnMult for Photoshop ------------------------------------------------------------ Reproduces the supplied After Effects plug-in's core operation: a = max(R, G, B) RGB = RGB / a (when 0 < a < 1) alpha = alpha * a Usage: 1. Open an RGB document and select the layer with a black background. 2. Run this file with File > Scripts > Browse... The source layer is kept (and hidden). The result is placed directly above it as a new raster layer. Set KEEP_ORIGINAL to false if preferred. */ (function () { var KEEP_ORIGINAL = true; var RESULT_SUFFIX = " - UnMult"; var SILENT_TEST = (typeof UNMULT_SILENT_TEST !== "undefined" && UNMULT_SILENT_TEST === true); var tempDoc = null; var sourceDoc = null; var sourceLayer = null; var visibilityState = []; function c2t(s) { return charIDToTypeID(s); } function s2t(s) { return stringIDToTypeID(s); } function rememberAndHide(container) { var i, item; for (i = 0; i < container.layers.length; i++) { item = container.layers[i]; visibilityState.push({ layer: item, visible: item.visible }); item.visible = false; if (item.typename === "LayerSet") { rememberAndHide(item); } } } function revealLayerPath(layer, doc) { var item = layer; while (item && item !== doc) { item.visible = true; item = item.parent; } } function restoreVisibility() { var i; for (i = visibilityState.length - 1; i >= 0; i--) { try { visibilityState[i].layer.visible = visibilityState[i].visible; } catch (ignore) {} } } function makeMaskFromSelection() { var desc = new ActionDescriptor(); var ref = new ActionReference(); desc.putClass(c2t("Nw "), c2t("Chnl")); ref.putEnumerated(c2t("Chnl"), c2t("Chnl"), c2t("Msk ")); desc.putReference(c2t("At "), ref); desc.putEnumerated(c2t("Usng"), c2t("UsrM"), c2t("RvlS")); executeAction(c2t("Mk "), desc, DialogModes.NO); } function applyLayerMask() { var desc = new ActionDescriptor(); var ref = new ActionReference(); ref.putEnumerated(c2t("Chnl"), c2t("Chnl"), c2t("Msk ")); desc.putReference(c2t("null"), ref); desc.putBoolean(c2t("Aply"), true); executeAction(c2t("Dlt "), desc, DialogModes.NO); } function removeBlackMatte() { // Photoshop 2026 still registers this native Action Manager event. // Calling it directly avoids relying on optional ExtendScript methods. executeAction(s2t("removeBlackMatte"), undefined, DialogModes.NO); } function channelReference(doc, channel, componentName) { var ref = new ActionReference(); if (componentName) { ref.putEnumerated(s2t("channel"), s2t("channel"), s2t(componentName)); } else { ref.putName(s2t("channel"), channel.name); } ref.putEnumerated(s2t("layer"), s2t("ordinal"), s2t("merged")); ref.putIdentifier(s2t("document"), doc.id); return ref; } function calculateToNewChannel(doc, firstChannel, secondChannel, firstComponent, secondComponent, blendType, invertFirst, invertSecond) { // Equivalent to Image > Calculations, Result: New Channel. var calculation = new ActionDescriptor(); calculation.putReference(s2t("to"), channelReference(doc, firstChannel, firstComponent)); calculation.putReference(s2t("source2"), channelReference(doc, secondChannel, secondComponent)); calculation.putBoolean(s2t("invert"), invertFirst === true); calculation.putBoolean(s2t("invertSource2"), invertSecond === true); calculation.putUnitDouble(s2t("opacity"), s2t("percentUnit"), 100.0); calculation.putEnumerated( s2t("calculation"), s2t("calculationType"), s2t(blendType) ); var make = new ActionDescriptor(); make.putClass(s2t("new"), s2t("channel")); make.putObject(s2t("using"), s2t("calculation"), calculation); executeAction(s2t("make"), make, DialogModes.NO); return app.activeDocument.activeChannels[0]; } function invertChannel(doc, channel) { doc.activeChannels = [channel]; executeAction(c2t("Invr"), undefined, DialogModes.NO); } function makeMaximumRGBSelection(doc, layer) { var channels = doc.componentChannels; if (!channels || channels.length < 3) { throw new Error("RGB channels are not available."); } // Photoshop Calculations uses different polarity for component channels // and alpha channels. Pass 1 yields 1-max(R,G), then is inverted. For // pass 2, Darken with an inverted Blue source yields max(max(R,G),B). doc.activeChannels = channels; var maxRG = calculateToNewChannel( doc, channels[0], channels[1], "red", "green", "lighten", false, false ); maxRG.name = "__UnMult_MaxRG__"; invertChannel(doc, maxRG); var maxRGB = calculateToNewChannel( doc, maxRG, channels[2], null, "blue", "darken", false, true ); maxRGB.name = "__UnMult_MaxRGB__"; doc.selection.load(maxRGB, SelectionType.REPLACE); doc.activeChannels = channels; maxRGB.remove(); maxRG.remove(); doc.activeLayer = layer; } try { if (app.documents.length === 0) { throw new Error("Open an image and select the black-background layer first."); } sourceDoc = app.activeDocument; if (sourceDoc.mode !== DocumentMode.RGB) { throw new Error("RGB mode is required. Convert the document to RGB first."); } if (sourceDoc.bitsPerChannel === BitsPerChannelType.THIRTYTWO) { throw new Error("32-bit/channel is not supported by JSX selections. Convert to 16-bit or 8-bit first."); } sourceLayer = sourceDoc.activeLayer; if (sourceLayer.typename !== "ArtLayer") { throw new Error("Select a layer, not a layer group."); } // Copy the selected layer's rendered appearance only. This also safely // handles smart objects, layer styles and pre-existing layer masks. rememberAndHide(sourceDoc); revealLayerPath(sourceLayer, sourceDoc); sourceDoc.activeLayer = sourceLayer; sourceDoc.selection.selectAll(); sourceDoc.selection.copy(true); sourceDoc.selection.deselect(); restoreVisibility(); visibilityState = []; tempDoc = app.documents.add( sourceDoc.width, sourceDoc.height, sourceDoc.resolution, "__UnMult_Temporary__", NewDocumentMode.RGB, DocumentFill.TRANSPARENT, sourceDoc.pixelAspectRatio, sourceDoc.bitsPerChannel ); tempDoc.paste(); var workLayer = tempDoc.activeLayer; makeMaximumRGBSelection(tempDoc, workLayer); makeMaskFromSelection(); tempDoc.selection.deselect(); // Applying the max(R,G,B) mask creates alpha. Removing the black matte // then performs RGB/max(R,G,B), which is the plug-in's unpremultiply. applyLayerMask(); removeBlackMatte(); var originalName = sourceLayer.name; var resultLayer = workLayer.duplicate(sourceDoc, ElementPlacement.PLACEATBEGINNING); app.activeDocument = sourceDoc; resultLayer.name = originalName + RESULT_SUFFIX; resultLayer.move(sourceLayer, ElementPlacement.PLACEBEFORE); resultLayer.visible = true; if (KEEP_ORIGINAL) { sourceLayer.visible = false; } else { sourceLayer.remove(); } sourceDoc.activeLayer = resultLayer; tempDoc.close(SaveOptions.DONOTSAVECHANGES); tempDoc = null; if (!SILENT_TEST) alert("NewLayer: " + resultLayer.name); } catch (err) { restoreVisibility(); try { if (sourceDoc) { app.activeDocument = sourceDoc; if (sourceLayer) sourceDoc.activeLayer = sourceLayer; } } catch (ignoreRestore) {} try { if (tempDoc) tempDoc.close(SaveOptions.DONOTSAVECHANGES); } catch (ignoreClose) {} if (SILENT_TEST) throw err; alert("UnMult failed:\n" + err.message); } }());