Highlighted

New script Auto Crop

Contributor ,
Aug 26, 2020

Copy link to clipboard

Copied

I have a new script, "Auto Crop", that uses Select Subject to crop the active image or a folder of images. Free to download here https://www.marspremedia.com/software/photoshop/auto-crop

Let me know any trouble with it, or other comments. Thanks, and enjoy.

William Campbell
Mars Premedia

Topics

Actions and scripting

Views

122

Likes

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

New script Auto Crop

Contributor ,
Aug 26, 2020

Copy link to clipboard

Copied

I have a new script, "Auto Crop", that uses Select Subject to crop the active image or a folder of images. Free to download here https://www.marspremedia.com/software/photoshop/auto-crop

Let me know any trouble with it, or other comments. Thanks, and enjoy.

William Campbell
Mars Premedia

Topics

Actions and scripting

Views

123

Likes

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
Most Valuable Participant ,
Aug 26, 2020

Copy link to clipboard

Copied

I downloaded ran it once it need work. Its saved in binary  I do not installscripts that I cn not reas who knows what the mat do.  So I deleted delete Auto crop.

Capture.jpg

JJMack

Likes

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
Reply
Loading...
Contributor ,
Aug 26, 2020

Copy link to clipboard

Copied

As I explained in the documentation, the script depends on Select Subject. It will only work as well as the image in question doesn't fail when using Select Subject. Also in the documentation, I referenced the original discussion that led to this script. Search "Script help: Crop based on select subject". I can (and will here) post the link, but I suggested searching because so often links to forum discussions change and later those links are broken. As of today, the original discussion is at https://community.adobe.com/t5/photoshop/script-help-crop-based-on-select-subject/m-p/10405382?page=...

 

The images in your post aren't what I envisioned as the sort this script was made to process (not to say it won't; but maybe not, and I can't test every posssible image out in the wild). See the earlier discussion examples, a child sports portrait, something with a single "model" (subject) that isn't difficult for Photoshop to discern from the background when invoking Select Subject. The script is better suited for these sort of images, especially cases when the user needs to process hundreds of them. Won't be 100% perfect, but it can certianly tackle the bulk of it, and save users some time, which was my intention.

 

About using jsxbin, to support a complex script I must ensure the user runs the precise code I've created (and tested myself, to the best that circumstances allow). So complex scripts I "lock up" in jsxbin, ensuring the code cannot be altered. If I provide complex scripts as jsx, users can read (and edit) them, and then who knows what the script might do.

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Most Valuable Participant ,
Aug 26, 2020

Copy link to clipboard

Copied

The script is not worth the risk then.

JJMack

Likes

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
Reply
Loading...
Contributor ,
Aug 26, 2020

Copy link to clipboard

Copied

Risk of what?

OK, I'll take the bait. Here is the text of the script. Have at it.

 

/*

Auto Crop
2020-08-26
Copyright 2020 William Campbell
All Rights Reserved
william@marspremedia.com
willcampbell7@gmail.com
https://www.marspremedia.com/contact

Permission to use and/or distribute this software in binary form without
modification for any purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

CHANGE LOG

Version 1.0 (200826)
a. Initial release.

*/

//@target photoshop

(function () {

    var title = "Auto Crop";
    var version = "1.0";

    // Preserve preferences.
    var displayDialogs = app.displayDialogs;
    var rulerUnits = app.preferences.rulerUnits;
    var backgroundColor = app.backgroundColor;

    // Script variables.
    var colorProfileNames;
    var defaultSettings;
    var docsOpen = [];
    var folderInput;
    var folderOutput;
    var lastUnitsIndex;

    var formats = [
        "JPG",
        "PNG",
        "PSD",
        "TIF"
    ];

    var measurementUnits = [
        "Pixels",
        "Inches",
        "Centimeters",
        "Millimeters",
        "Points",
        "Picas"
    ];

    var measurementUnitsShort = [
        "px",
        "in",
        "cm",
        "mm",
        "pt",
        "pc"
    ];

    var qualities = [
        "0",
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "10",
        "11",
        "12"
    ];

    // Reusable UI variables.
    var g; // group
    var g1; // group
    var g2; // group
    var g3; // group
    var g4; // group
    var p; // panel
    var w; // window

    // Permanant UI variables.
    var btnCancel;
    var btnDeleteSettings;
    var btnFolderOutput;
    var btnOk;
    var btnSaveSettings;
    var cbConvert;
    var cbFlatten;
    var grpQuality;
    var inpHeight;
    var inpMargin;
    var inpResolution;
    var inpSuffix;
    var inpWidth;
    var listFormat;
    var listProfile;
    var listQuality;
    var listSettings;
    var listUnitsWidth;
    var panelMargins = [16, 16, 16, 16];
    var pnlOutput;
    var pnlResolution;
    var rbActive;
    var rbFolderInput;
    var rbResolutionAsIs;
    var rbResolutionResample;
    var txtFolderInput;
    var txtFolderOutput;
    var txtUnitsHeight;
    var txtUnitsMargin;
    var txtUnitsResolution;

    // Execution variables.
    var doneInfo = "";
    var doneMessage = "";
    var count = 0;
    var targetHeight;
    var targetMargin;
    var targetWidth;

    // LANGUAGE EXTENSIONS

    if (!Array.prototype.indexOf) {
        Array.prototype.indexOf = function (x) {
            for (var i = 0; i < this.length; i++) {
                if (this[i] == x) {
                    return i;
                }
            }
            return -1;
        };
    }

    /* eslint-disable */
    /* beautify preserve:start */
    // json2.js 2016-10-28
    if(typeof JSON!=="object"){JSON={};}(function(){"use strict";var rx_one=/^[\],:{}\s]*$/;var rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rx_four=/(?:^|:|,)(?:\s*\[)+/g;var rx_escapable=/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;var rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;function f(n){return n<10?"0"+n:n;}function this_value(){return this.valueOf();}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null;};Boolean.prototype.toJSON=this_value;Number.prototype.toJSON=this_value;String.prototype.toJSON=this_value;}var gap;var indent;var meta;var rep;function quote(string){rx_escapable.lastIndex=0;return rx_escapable.test(string)?"\""+string.replace(rx_escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4);})+"\"":"\""+string+"\"";}function str(key,holder){var i;var k;var v;var length;var mind=gap;var partial;var value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key);}if(typeof rep==="function"){value=rep.call(holder,key,value);}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null";}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||"null";}v=partial.length===0?"[]":gap?"[\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"]":"["+partial.join(",")+"]";gap=mind;return v;}if(rep&&typeof rep==="object"){length=rep.length;for(i=0;i<length;i+=1){if(typeof rep[i]==="string"){k=rep[i];v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v);}}}}else{for(k in value){if(Object.prototype.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?": ":":")+v);}}}}v=partial.length===0?"{}":gap?"{\n"+gap+partial.join(",\n"+gap)+"\n"+mind+"}":"{"+partial.join(",")+"}";gap=mind;return v;}}if(typeof JSON.stringify!=="function"){meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r","\"":"\\\"","\\":"\\\\"};JSON.stringify=function(value,replacer,space){var i;gap="";indent="";if(typeof space==="number"){for(i=0;i<space;i+=1){indent+=" ";}}else if(typeof space==="string"){indent=space;}rep=replacer;if(replacer&&typeof replacer!=="function"&&(typeof replacer!=="object"||typeof replacer.length!=="number")){throw new Error("JSON.stringify");}return str("",{"":value});};}if(typeof JSON.parse!=="function"){JSON.parse=function(text,reviver){var j;function walk(holder,key){var k;var v;var value=holder[key];if(value&&typeof value==="object"){for(k in value){if(Object.prototype.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v;}else{delete value[k];}}}}return reviver.call(holder,key,value);}text=String(text);rx_dangerous.lastIndex=0;if(rx_dangerous.test(text)){text=text.replace(rx_dangerous,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4);});}if(rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,""))){j=eval("("+text+")");return(typeof reviver==="function")?walk({"":j},""):j;}throw new SyntaxError("JSON.parse");};}}());    /* beautify preserve:end */
    /* beautify preserve:end */
    /* eslint-enable */

    // SETUP

    // Compile list of color profile names.
    colorProfileNames = loadColorProfiles();
    colorProfileNames.indexOf = function (x) {
        for (var i = 0; i < this.length; i++) {
            if (this[i] == x) {
                return i;
            }
        }
        return -1;
    };

    // Compile list of open documents.
    docsOpen = [];
    (function () {
        for (var i = 0; i < app.documents.length; i++) {
            try {
                docsOpen.push(File.decode(app.documents[i].fullName));
            } catch (e) {
                // Ignore. Probably never saved, doesn't have fullName property.
            }
        }
    })();

    // DEFAULTS

    defaultSettings = {
        name: "[Default]",
        width: "5",
        height: "7",
        margin: "0.5",
        resample: false,
        resolution: "300",
        units: "Inches",
        format: "JPG",
        quality: "10",
        convert: false,
        profile: "sRGB IEC61966-2.1",
        suffix: "_5x7",
        flatten: false
    };

    // LOG

    var log = {
        entries: [],
        file: null,
        // Default write log to user desktop.
        // Preferrably set 'log.path' to a meaningful
        // location in later code once location exists.
        path: Folder.desktop,
        add: function (message) {
            this.entries.push(message);
        },
        cancel: function () {
            if (log.entries.length) {
                log.add("User cancelled.");
            }
        },
        write: function () {
            var contents;
            var d;
            var fileName;
            var padZero = function (v) {
                return ("0" + v).slice(-2);
            };
            if (!this.entries.length) {
                // No log entries to report.
                this.file = null;
                return;
            }
            contents = this.entries.join("\r");
            // Create file name.
            d = new Date();
            fileName =
                title +
                " Log " +
                d.getFullYear() +
                "-" +
                padZero(d.getMonth() + 1) +
                "-" +
                padZero(d.getDate()) +
                "-" +
                padZero(d.getHours()) +
                padZero(d.getMinutes()) +
                padZero(String(d.getSeconds()).substr(0, 2)) +
                ".txt";
            // Open and write log file.
            this.file = new File(this.path + "/" + fileName);
            this.file.encoding = "UTF-8";
            try {
                if (!this.file.open("w")) {
                    throw new Error("Failed to open log file.");
                }
                if (!this.file.write(contents)) {
                    throw new Error("Failed to write log file.");
                }
                this.file.close();
            } catch (e) {
                this.file = null;
                throw e;
            }
            // Log successfully written.
            // log.file == true (not null) indicates a log was written.
        }
    };

    // SETTINGS

    // The functions 'apply' and 'current' reference UI elements
    // that are associated with this settings object. Any change to
    // UI elements requires changes within these functions as well.

    // This object assumes the script has defined (or will define)
    // a DropDownList assigned the name 'listSettings'.

    var settings = {
        items: [],
        add: function () {
            var btnOk;
            var btnCancel;
            var currentSettings;
            var g; // group
            var index;
            var w; // window
            w = new Window("dialog", "Save Settings");
            w.margins = [24, 12, 24, 24];
            w.alignChildren = "fill";
            w.add("statictext", undefined, "Save current settings as:");
            w.name = w.add("edittext");
            w.name.characters = 30;
            g = w.add("group");
            g.alignment = "center";
            btnOk = g.add("button", undefined, "OK");
            btnCancel = g.add("button", undefined, "Cancel");
            w.name.active = true;
            btnOk.onClick = function () {
                if (!w.name.text.length) {
                    alert("Enter a name for the settings.");
                    return;
                }
                w.close(1);
            };
            btnCancel.onClick = function () {
                w.name.text = "";
                w.close(0);
            };
            if (w.show() == 1) {
                index = this.indexOf(w.name.text);
                if (index > -1) {
                    if (!confirm("Name already exists. Replace?", true, "Save settings")) {
                        return -1;
                    }
                }
                currentSettings = this.current();
                currentSettings.name = w.name.text;
                if (index > -1) {
                    this.items[index] = currentSettings;
                } else {
                    this.items.push(currentSettings);
                    this.sort();
                    index = this.indexOf(w.name.text);
                }
                this.save();
                return index;
            }
            return -1;
        },
        apply: function (o) {
            // Any change to UI elements requires change here as well.
            var index;
            inpWidth.text = o.width || defaultSettings.width;
            inpHeight.text = o.height || defaultSettings.height;
            inpMargin.text = o.margin || defaultSettings.margin;
            rbResolutionAsIs.value = !o.resample;
            rbResolutionResample.value = !rbResolutionAsIs.value;
            inpResolution.text = o.resolution || defaultSettings.resolution;
            index = measurementUnits.indexOf(o.units);
            if (index === -1) {
                // Look for default.
                index = measurementUnits.indexOf(defaultSettings.units);
            }
            listUnitsWidth.selection = index > -1 ? index : 1; // Default "Inches"
            index = formats.indexOf(o.format);
            if (index === -1) {
                // Look for default.
                index = formats.indexOf(defaultSettings.format);
            }
            listFormat.selection = index > -1 ? index : 0; // Default "JPG"
            index = qualities.indexOf(o.quality);
            if (index === -1) {
                // Look for default.
                index = qualities.indexOf(defaultSettings.quality);
            }
            listQuality.selection = index > -1 ? index : 10; // Default "10"
            cbConvert.value = o.convert;
            index = colorProfileNames.indexOf(o.profile);
            if (index === -1) {
                // Look for default.
                index = colorProfileNames.indexOf(defaultSettings.profile);
            }
            listProfile.selection = index > -1 ? index : null; // Default null
            inpSuffix.text = o.suffix || "";
            cbFlatten.value = o.flatten;
        },
        current: function () {
            // Any change to UI elements requires change here as well.
            return {
                name: listSettings.selection ? listSettings.selection.text : "",
                width: inpWidth.text,
                height: inpHeight.text,
                margin: inpMargin.text,
                resample: rbResolutionResample.value,
                units: listUnitsWidth.selection ? listUnitsWidth.selection.text : "",
                format: listFormat.selection ? listFormat.selection.text : "",
                quality: listQuality.selection ? listQuality.selection.text : "",
                convert: cbConvert.value,
                profile: listProfile.selection ? listProfile.selection.text : "",
                suffix: inpSuffix.text,
                flatten: cbFlatten.value
            };
        },
        file: function () {
            var vendorFolder = new Folder(Folder.userData + "/Mars Premedia");
            if (!vendorFolder.exists) {
                vendorFolder.create();
            }
            return new File(vendorFolder + "/" + title + ".json");
        },
        indexOf: function (name) {
            for (var i = 0; i < this.items.length; i++) {
                if (this.items[i].name == name) {
                    return i;
                }
            }
            return -1;
        },
        load: function () {
            var file = this.file();
            var index;
            var o; // Important! Start undefined.
            if (file.exists) {
                try {
                    if (!file.open("r")) {
                        throw new Error("Failed to open file.");
                    }
                    o = JSON.parse(file.read());
                    file.close();
                    if (!o.items) {
                        throw new Error("Unrecognized data.");
                    }
                } catch (e) {
                    // File read error or faulty json.
                    alert("Error reading settings file." + "\n" + "Values will be set to default." + "\n\n" + "Error" + ": " + e.message);
                }
            }
            if (o && o.items) {
                // Use saved settings.
                this.items = o.items;
                this.last = o.last || defaultSettings;
            } else {
                // Either settings file does not exist, or error reading it.
                // If not exist, assume first time running script.
                // In either case, apply default settings and save.
                this.items[0] = defaultSettings;
                this.last = defaultSettings;
                this.save();
            }
            this.apply(this.last);
            if (this.indexOf("[Default]") == -1) {
                // Default does not exist. Add it.
                this.items.push(defaultSettings);
            }
            // Update UI.
            this.update();
            // Set list selection if last is a saved settings.
            index = this.indexOf(this.last.name);
            listSettings.selection = index > -1 ? index : null;
        },
        preserveLast: function () {
            this.last = this.current();
            this.save();
        },
        remove: function (index) {
            this.items.splice(index, 1);
            this.save();
        },
        save: function () {
            var file = this.file();
            try {
                if (!file.open("w")) {
                    throw new Error("Failed to open file.");
                }
                file.write(JSON.stringify(settings));
                file.close();
            } catch (e) {
                alert("Error saving settings file." + "\n" + "Error" + ": " + e.message);
            }
        },
        sort: function () {
            this.items.sort(function (a, b) {
                return (a.name > b.name) ? 1 : ((a.name < b.name) ? -1 : 0);
            });
        },
        update: function () {
            var i;
            listSettings.removeAll();
            for (i = 0; i < this.items.length; i++) {
                listSettings.add("item", this.items[i].name);
            }
        }
    };

    // CREATE USER INTERFACE

    w = new Window("dialog", title);
    w.alignChildren = "fill";

    // Panel 'Process'
    p = w.add("panel", undefined, "Process");
    p.alignChildren = "left";
    p.margins = panelMargins;
    g1 = p.add("group");
    g1.orientation = "column";
    g1.alignChildren = "left";
    rbActive = g1.add("radiobutton", undefined, "Active image");
    g2 = g1.add("group");
    rbFolderInput = g2.add("radiobutton", undefined, "Folder...");
    txtFolderInput = g2.add("statictext", undefined, undefined, {
        truncate: "middle"
    });
    txtFolderInput.preferredSize = [350, -1];

    // Panel 'Size'
    p = w.add("panel", undefined, "Size");
    p.alignChildren = "left";
    p.margins = panelMargins;
    // Group labels and inputs.
    g1 = p.add("group");
    g1.alignChildren = "left";
    // Group labels.
    g2 = g1.add("group");
    g2.orientation = "column";
    g2.alignChildren = "right";
    g2.spacing = 12;
    g2.add("statictext", undefined, "Width:");
    g2.add("statictext", undefined, "Height:");
    g2.add("statictext", undefined, "Minimum margin:");
    // Group inputs.
    g2 = g1.add("group");
    g2.spacing = 5;
    g2.orientation = "column";
    g2.alignChildren = "left";
    // Group input width.
    g3 = g2.add("group");
    inpWidth = g3.add("edittext");
    inpWidth.characters = 9;
    listUnitsWidth = g3.add("dropdownlist", undefined, undefined, {
        items: measurementUnits
    });
    listUnitsWidth.preferredSize = [-1, 25];
    // Group input height.
    g3 = g2.add("group");
    inpHeight = g3.add("edittext");
    inpHeight.characters = 9;
    g4 = g3.add("group");
    g4.margins = [4, -1, -1, -1];
    txtUnitsHeight = g4.add("statictext", undefined, "Inches");
    txtUnitsHeight.preferredSize = [100, -1];
    // Group input minimum margin.
    g3 = g2.add("group");
    inpMargin = g3.add("edittext");
    inpMargin.characters = 9;
    g4 = g3.add("group");
    g4.margins = [4, -1, -1, -1];
    txtUnitsMargin = g4.add("statictext", undefined, "Inches");
    txtUnitsMargin.preferredSize = [100, -1];

    // Panel 'Resolution'
    p = w.add("panel", undefined, "Resolution");
    p.alignChildren = "left";
    p.margins = panelMargins;
    rbResolutionAsIs = p.add("radiobutton", undefined, "As-is");
    // Group resample.
    g1 = p.add("group");
    g1.alignChildren = "left";
    // Group radio button and label.
    g2 = g1.add("group");
    g2.orientation = "column";
    g2.alignChildren = "left";
    g2.spacing = 12;
    rbResolutionResample = g2.add("radiobutton", undefined, "Resample:");
    // Group inputs and units.
    g2 = g1.add("group");
    g2.alignChildren = "left";
    g2.spacing = 5;
    inpResolution = g2.add("edittext");
    inpResolution.characters = 9;
    txtUnitsResolution = g2.add("statictext", undefined, "Pixels/Inch");
    pnlResolution = p;

    // Panel 'Output'
    p = w.add("panel", undefined, "Output");
    p.alignChildren = "left";
    p.margins = panelMargins;
    g1 = p.add("group");
    g1.add("statictext", undefined, "Format:");
    listFormat = g1.add("dropdownlist", undefined, undefined, {
        items: formats
    });
    grpQuality = g1.add("group");
    grpQuality.margins = [18, 0, 0, 0];
    grpQuality.add("statictext", undefined, "Quality:");
    listQuality = grpQuality.add("dropdownlist", undefined, undefined, {
        items: qualities
    });
    g1 = p.add("group");
    cbConvert = g1.add("checkbox", undefined, "Convert to profile:");
    listProfile = g1.add("dropdownlist", undefined, undefined, {
        items: colorProfileNames
    });
    listProfile.preferredSize = [250, -1];
    g1 = p.add("group");
    g1.add("statictext", undefined, "Original file name +");
    inpSuffix = g1.add("edittext");
    inpSuffix.characters = 12;
    cbFlatten = g1.add("checkbox", undefined, "Flatten");
    g1 = p.add("group");
    btnFolderOutput = g1.add("button", undefined, "Folder...");
    txtFolderOutput = g1.add("statictext", undefined, "", {
        truncate: "middle"
    });
    txtFolderOutput.preferredSize = [337, -1];
    pnlOutput = p;

    // Panel 'Settings'
    p = w.add("panel", undefined, "Settings");
    p.alignChildren = "center";
    p.margins = panelMargins;
    g = p.add("group");
    g.orientation = "row";
    g.alignChildren = "left";
    g.add("statictext", undefined, "Load:");
    listSettings = g.add("dropdownlist");
    listSettings.preferredSize = [200, 25];
    btnDeleteSettings = g.add("button", undefined, "Delete");
    btnSaveSettings = g.add("button", undefined, "Save");

    // Action Buttons
    g = w.add("group");
    g.alignment = "center";
    btnOk = g.add("button", undefined, "OK");
    btnCancel = g.add("button", undefined, "Cancel");

    // Panel 'Support' (no label)
    p = w.add("panel");
    p.add("statictext", undefined, "Version " + version + " - Copyright 2020 William Campbell");
    p.add("statictext", undefined, "Support or custom programming contact william@marspremedia.com");

    // SET DEFAULT VALUES / RESTORE LAST VALUES USED.
    // MUST PRECEDE UI EVENT HANDLERS SO THEY DON'T FIRE WHILE SETTING VALUES.

    try {
        app.activeDocument.fullName; // Throws error if undefined.
        rbActive.value = true;
    } catch (e) {
        // Nothing open or what is active has never been saved.
        // Disable 'Active image' option.
        rbActive.value = false;
        rbActive.enabled = false;
    }

    settings.load();
    configureUi();
    lastUnitsIndex = listUnitsWidth.selection.index;

    // UI ELEMENT EVENT HANDLERS

    // Panel 'Process'
    rbActive.onClick = function () {
        rbFolderInput.value = false;
        configureUi();
    };
    rbFolderInput.onClick = function () {
        var f = Folder.selectDialog("Select folder");
        if (f) {
            txtFolderInput.text = Folder.decode(f.fsName);
            folderInput = f;
            f = new Folder(f + "/Cropped");
            txtFolderOutput.text = Folder.decode(f.fsName);
            folderOutput = f;
            setSuffix();
        }
        rbActive.value = false;
        configureUi();
    };

    // Panel 'Size'
    inpWidth.onChange = changeSize;
    inpHeight.onChange = changeSize;
    inpMargin.onChange = function () {
        validateNumeric(this);
    };
    listUnitsWidth.onChange = function () {
        var mu1;
        var mu2;
        var units = listUnitsWidth.selection.text;
        var index = listUnitsWidth.selection.index;
        txtUnitsHeight.text = units;
        txtUnitsMargin.text = units;
        if (index !== lastUnitsIndex) {
            // Units have changed.
            mu1 = measurementUnitsShort[lastUnitsIndex];
            mu2 = measurementUnitsShort[index];
            inpWidth.text = String(UnitValue(Number(inpWidth.text), mu1).as(mu2));
            inpHeight.text = String(UnitValue(Number(inpHeight.text), mu1).as(mu2));
            inpMargin.text = String(UnitValue(Number(inpMargin.text), mu1).as(mu2));
            lastUnitsIndex = index;

        }
        changeOption();
    };

    // Panel 'Resolution'
    rbResolutionAsIs.onClick = function () {
        rbResolutionResample.value = false;
        changeOption();
    };
    rbResolutionResample.onClick = function () {
        rbResolutionAsIs.value = false;
        changeOption();
    };
    inpResolution.onChange = function () {
        validateNumeric(this);
    };

    // Panel 'Output'
    listFormat.onChange = changeOption;
    listQuality.onChange = changeOption;
    cbConvert.onClick = changeOption;
    listProfile.onChange = changeOption;
    inpSuffix.onChange = function () {
        // Trim.
        this.text = this.text.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "");
        // Remove and periods.
        this.text = this.text.replace(/\.+/g, "");
        // Remove illegal characters.
        var s = this.text.replace(/[\/\\:*?"<>|]/g, "");
        if (this.text != s) {
            this.text = s;
            alert("Illegal characters detected and removed.");
        }
        changeOption();
    };
    btnFolderOutput.onClick = function () {
        var f = Folder.selectDialog("Select folder");
        if (f) {
            txtFolderOutput.text = Folder.decode(f.fsName);
            folderOutput = f;
        }
    };

    // Panel 'Settings'
    listSettings.onChange = function () {
        if (this.selection != null) {
            // Lock settings so change events don't fire.
            // configureUI() will unlock when done.
            settings.lock = true;
            settings.apply(settings.items[this.selection.index]);
            configureUi();
        }
    };
    btnDeleteSettings.onClick = function () {
        var index = listSettings.selection.index;
        if (index > -1 && confirm("Delete settings" + " \"" + settings.items[index].name + "\" ", true)) {
            settings.remove(index);
            listSettings.remove(index);
            listSettings.selection = null;
            this.enabled = false;
        }
    };
    btnSaveSettings.onClick = function () {
        var index = settings.add();
        if (index > -1) {
            settings.update();
            listSettings.selection = index;
            btnDeleteSettings.enabled = true;
        }
    };

    // Action Buttons
    btnOk.onClick = function () {
        if (!(rbActive.value || rbFolderInput.value)) {
            alert("Select process");
            return;
        }
        if (rbFolderInput.value && !(txtFolderInput.text && txtFolderOutput.text)) {
            alert("Select input/output folders");
            return;
        }
        w.close(1);
    };
    btnCancel.onClick = function () {
        w.close(0);
    };

    // DISPLAY THE DIALOG

    if (w.show() == 1) {
        try {
            settings.preserveLast();
            process();
            if (rbFolderInput.value) {
                // Processing folder.
                doneMessage =
                    "Processed" +
                    " " +
                    count +
                    " " +
                    (count == 1 ? "file" : "files");
            }
        } catch (e) {
            if (/User cancelled/.test(e.message)) {
                log.cancel();
                doneMessage = null;
            } else {
                doneMessage = "An error has occurred.";
                doneInfo = errorString(e);
            }
        }
        try {
            log.write();
        } catch (e) {
            // Error writing log.
            doneInfo += (doneInfo.length ? "\n" : "");
            doneInfo += errorString(e);
        }
        // Restore app preferences.
        app.backgroundColor = backgroundColor;
        app.preferences.rulerUnits = rulerUnits;
        app.displayDialogs = displayDialogs;

        if (doneMessage) { // Didn't cancel.
            notify(doneMessage, doneInfo);
        } else if (log.file) {
            // Without notify dialog to offer it, force open log if exists.
            log.file.execute();
        }
    }

    //====================================================================
    //               END PROGRAM EXECUTION, BEGIN FUNCTIONS
    //====================================================================

    function changeOption() {
        // Only if settings unlocked.
        if (!settings.lock) {
            listSettings.selection = null;
            configureUi();
        }
    }

    function closeAllWasNotOpen() {
        for (var i = app.documents.length - 1; i > -1; i--) {
            closeWasNotOpen(app.documents[i]);
        }
    }

    function closeWasNotOpen(d) {
        var fullName;
        var i;
        try {
            fullName = File.decode(d.fullName);
            for (i = 0; i < docsOpen.length; i++) {
                if (docsOpen[i] == fullName) {
                    // Document was open at time script
                    // was launched; don't close it.
                    return;
                }
            }
        } catch (e) {
            // Ignore; doc is likely unsaved dupe, which
            // lacks property 'fullName', throwing error.
            // Let execution continue below, to close doc.
        }
        try {
            d.close(SaveOptions.DONOTSAVECHANGES);
        } catch (e) {
            // Ignore. Probably already closed.
        }
    }

    function changeSize() {
        validateNumeric(this);
        setSuffix();
    }

    function configureUi() {
        if (rbActive.value) {
            pnlOutput.enabled = false;
        } else {
            pnlOutput.enabled = true;
        }
        if (listUnitsWidth.selection.text === "Pixels") {
            rbResolutionAsIs.value = true;
            rbResolutionResample.value = false;
            pnlResolution.enabled = false;
        } else {
            pnlResolution.enabled = true;
        }
        inpResolution.enabled = rbResolutionResample.value;
        txtUnitsResolution.enabled = inpResolution.enabled;
        if (listFormat.selection.index === 0) { // "JPG"
            grpQuality.visible = true;
            cbFlatten.value = true;
            cbFlatten.enabled = false;
        } else {
            grpQuality.visible = false;
            cbFlatten.enabled = true;
        }
        if (listFormat.selection.index === 1) { // "PNG"
            listProfile.selection = colorProfileNames.indexOf("sRGB IEC61966-2.1");
            listProfile.enabled = false;
            cbConvert.value = true;
            cbConvert.enabled = false;
        } else {
            cbConvert.enabled = true;
            if (cbConvert.value) {
                listProfile.enabled = true;
            } else {
                listProfile.enabled = false;
            }
        }
        if (rbActive.value) {
            // Active images uncheck convert and flatten.
            cbConvert.value = false;
            cbFlatten.value = false;
        }
        // Disable settings 'delete' button if null or default is loaded.
        btnDeleteSettings.enabled = !(listSettings.selection == null || listSettings.selection.text == "[Default]");
        // Unlock settings.
        settings.lock = false;
    }

    function errorString(e) {
        return "ERROR" + " " + "Line" + " " + e.line + ": " + e.message;
    }

    function getFiles(folder, subfolders) {
        // folder = folder object, not folder name.
        // subfolder = bool, include subfolders.
        var f;
        var files;
        var i;
        var result = [];
        files = folder.getFiles();
        for (i = 0; i < files.length; i++) {
            f = files[i];
            if (/\.(app|doc|exe|indd|indb|jsx|jsxbin|log|rtf|txt|zip)$/i.test(f.name)) {
                // Ignore extensions Photoshop won't open.
                continue;
            }
            if (f instanceof Folder && subfolders) {
                // Recurse into folder.
                result = result.concat(getFiles(f, subfolders));
            } else if (f instanceof File && !f.hidden) {
                result.push(f);
            }
        }
        return result;
    }

    function loadColorProfiles() {
        var winDriveLetter = Folder.appData.toString().substr(0, 2);
        var files = [];
        var folders = [
            // Mac
            "/Library/Application Support/Adobe/Color/Profiles",
            "/Library/Application Support/Adobe/Color/Profiles/Recommended",
            "/Library/ColorSync/Profiles",
            "/System/Library/ColorSync/Profiles",
            "~/Library/ColorSync/Profiles",
            // Windows
            winDriveLetter + "/Program Files/Common Files/Adobe/Color/Profiles",
            winDriveLetter + "/Program Files/Common Files/Adobe/Color/Profiles/Recommended",
            winDriveLetter + "/Program Files (x86)/Common Files/Adobe/Color/Profiles",
            winDriveLetter + "/Program Files (x86)/Common Files/Adobe/Color/Profiles/Recommended",
            winDriveLetter + "/Windows/System32/spool/drivers/color"
        ];
        var i;
        var ii;
        var list = [];
        list.pushUnique = function (x) {
            for (var i = 0; i < this.length; i++) {
                if (this[i] == x) {
                    return;
                }
            }
            this.push(x);
        };
        for (i = 0; i < folders.length; i++) {
            files = getProfiles(new Folder(folders[i]));
            for (ii = 0; ii < files.length; ii++) {
                list.pushUnique(files[ii]);
            }
        }
        // Add standard grayscale profiles.
        list.push("Dot Gain 10%");
        list.push("Dot Gain 15%");
        list.push("Dot Gain 20%");
        list.push("Dot Gain 30%");
        list.push("Gray Gamma 1.8");
        list.push("Gray Gamma 2.2");
        // Sort the list a to z.
        list.sort(function (a, b) {
            return a.toLowerCase() > b.toLowerCase();
        });
        return list;

        //==== loadColorProfiles() CHILD FUNCTION ====//

        function getProfiles(folder) {
            // 'folder' is Folder object, not name.
            // Not recursive.
            var f;
            var files;
            var i;
            var list = [];
            files = folder.getFiles();
            for (i = 0; i < files.length; i++) {
                f = files[i];
                if (!(f instanceof File) || f.hidden || !/\.ic(c|m)$/i.test(f.name)) {
                    continue;
                }
                // Include only visible .icc/.icm files.
                list.push(profileName(f));
            }
            return list;
        }

        //==== loadColorProfiles() CHILD FUNCTION ====//

        function profileName(file) {
            var data;
            var index;
            var len;
            var match;
            var name;
            file.encoding = "BINARY";
            file.open("r");
            // First kilobyte of file is enough.
            // Profile name isn't beyond that.
            data = file.read(1024);
            file.close();
            match = /\x00\x00desc|\x90\x91desc/.exec(data);
            if (!match) {
                // Didn't find match. Return base file name.
                return File.decode(file.name).replace(/\.ic(c|m)$/i, "");
            }
            // Length of profile name string is stored 13 bytes past begin of match.
            index = RegExp.leftContext.length + 13;
            len = data.charCodeAt(index);
            // Profile name string follows, for 'len' characters minus one.
            name = data.substr(++index, --len);
            return name;
        }

    }

    function logAddFileMessage(fileName, message) {
        log.add(File.decode(fileName));
        if (message instanceof Array) {
            while (message.length) {
                log.add("----> " + message.shift());
            }
        } else {
            log.add("----> " + message);
        }
    }

    function mergeLayers() {
        // Dependencies:
        //    rasterizeLayers()
        //
        var d = app.activeDocument;
        var desc1;
        var layerDesc;
        var ref1;
        if (d.layers.length == 1 && d.layers[0].isBackgroundLayer) {
            // Image is already flattened -- nothing to do.
            return;
        }
        // Delete hidden layers.
        try {
            desc1 = new ActionDescriptor();
            ref1 = new ActionReference();
            ref1.putEnumerated(charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), stringIDToTypeID("hidden"));
            desc1.putReference(charIDToTypeID('null'), ref1);
            executeAction(charIDToTypeID('Dlt '), desc1, DialogModes.NO);
        } catch (e) {
            // Ignore.
        }
        rasterizeLayers(d);
        if (d.layerSets.length || d.layers.length > 1) {
            // More than a single layer. Merge visible into single layer.
            executeAction(stringIDToTypeID("mergeVisible"), undefined, DialogModes.NO);
        }
        // Check for raster mask.
        // If found and enabled, apply mask.
        // If found and disabled, delete mask.
        ref1 = new ActionReference();
        ref1.putEnumerated(charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), charIDToTypeID('Trgt'));
        layerDesc = executeActionGet(ref1);
        if (layerDesc.getBoolean(stringIDToTypeID("hasUserMask"))) {
            if (layerDesc.getBoolean(stringIDToTypeID("userMaskEnabled"))) {
                // Apply layer mask.
                desc1 = new ActionDescriptor();
                ref1 = new ActionReference();
                ref1.putEnumerated(charIDToTypeID('Chnl'), charIDToTypeID('Ordn'), charIDToTypeID('Msk '));
                desc1.putReference(charIDToTypeID('null'), ref1);
                desc1.putBoolean(charIDToTypeID('Aply'), true);
                executeAction(charIDToTypeID('Dlt '), desc1, DialogModes.NO);
            } else {
                // Delete layer mask.
                desc1 = new ActionDescriptor();
                ref1 = new ActionReference();
                ref1.putEnumerated(charIDToTypeID('Chnl'), charIDToTypeID('Chnl'), charIDToTypeID('Msk '));
                desc1.putReference(charIDToTypeID('null'), ref1);
                executeAction(charIDToTypeID('Dlt '), desc1, DialogModes.NO);
            }
        }
        // Rename single merged layer.
        d.layers[0].name = "Layer 1";
    }

    function notify(message, info) {
        // Script only logs concerns and/or errors, so include verbiage
        // "Alerts reported", absent in scripts that always log.
        var btnOk;
        var btnOpenLog;
        var g;
        var t;
        var w;
        var fontUI;
        w = new Window("dialog");
        fontUI = w.graphics.font;
        w.alignChildren = "left";
        t = w.add("statictext", undefined, message);
        t.graphics.font = ScriptUI.newFont(fontUI.name, ScriptUI.FontStyle.BOLD, fontUI.size + 1);
        if (info) {
            t = w.add("statictext", undefined, info, {
                multiline: true
            });
        }
        if (log.file) {
            t = w.add("statictext", undefined, "Alerts reported. See log for details:" + "\n" + File.decode(log.file.fsName), {
                multiline: true
            });
        }
        t.preferredSize = [400, -1];
        g = w.add("group");
        g.alignment = "center";
        btnOk = g.add("button", undefined, "OK");
        btnOk.onClick = function () {
            w.close(11);
        };
        if (log.file) {
            btnOpenLog = g.add("button", undefined, "Open Log");
            btnOpenLog.onClick = function () {
                w.close(12);
            };
        }
        if (w.show() == 12) {
            log.file.execute();
        }
    }

    function process() {
        // var count parent scope.
        // var folderInput parent scope.
        // var folderOutput parent scope.
        // var targetHeight parent scope.
        // var targetMargin parent scope.
        // var targetWidth parent scope.
        var doc;
        var file;
        var fileName;
        var files;
        var fullName;
        var i;
        var ii;
        var mu;
        var results;
        var white;

        app.displayDialogs = DialogModes.NO;
        app.preferences.rulerUnits = Units.PIXELS;

        // Set app background color.
        white = new SolidColor();
        white.rgb.hexValue = "FFFFFF";
        app.backgroundColor = white;

        // Set target dimensions.
        mu = measurementUnitsShort[listUnitsWidth.selection.index];
        targetHeight = UnitValue(Number(inpHeight.text), mu).as("in");
        targetMargin = UnitValue(Number(inpMargin.text), mu).as("in");
        targetWidth = UnitValue(Number(inpWidth.text), mu).as("in");

        // Process file or folder.
        if (rbActive.value) {
            // Process the active image.
            progress();
            doc = app.activeDocument;
            log.path = doc.path;
            results = processDoc(doc);
            if (results.length) {
                logAddFileMessage(doc.fullName.fsName, results);
            }
            progress.close();
        } else {
            // Process a folder of images.
            log.path = folderInput;
            files = getFiles(folderInput, false);
            if (!files.length) {
                doneMessage = "No files found in selected folder.";
                return;
            }
            // Loop through all images.
            progress(files.length);
            try {
                for (i = 0; i < files.length; i++) {
                    doc = null;
                    file = files[i];
                    fileName = File.decode(file.name);
                    progress.message(fileName);
                    testCancel();
                    // Check if file is an open document.
                    // If so, make a dupe.
                    try {
                        fullName = File.decode(file.fullName);
                        for (ii = 0; ii < docsOpen.length; ii++) {
                            if (docsOpen[ii] === fullName) {
                                // Image is currently open.
                                // Make and process a dupe of it.
                                doc = app.documents[ii].duplicate(fileName);
                                break;
                            }
                        }
                    } catch (e) {
                        // Ignore.
                    }
                    try {
                        if (!doc) {
                            doc = app.open(file);
                        }
                        app.refresh();
                        results = processDoc(doc);
                        if (results.length) {
                            logAddFileMessage(file.fsName, results);
                        }
                        app.refresh();
                        saveAndClose(doc);
                    } catch (e) {
                        if (/User cancelled/.test(e.message)) {
                            throw e;
                        }
                        if (/open options are incorrect/.test(e.message)) {
                            // Trying to open non-Photoshop image.
                            // Ignore.
                        } else {
                            // Log the error.
                            logAddFileMessage(file.fsName, errorString(e));
                        }
                    } finally {
                        // Close any files not already open, error or not.
                        closeAllWasNotOpen();
                    }
                    progress.increment();
                    count++;
                }
            } finally {
                progress.close();
            }
        }
    }

    function processDoc(doc) {
        // var targetHeight parent scope.
        // var targetMargin parent scope.
        // var targetWidth parent scope.
        var bounds;
        var resHigh;
        var resWide;
        var resolution;
        var results = [];

        // Make bottom layer a layer, not background.
        // (to preserve pixels while resizing canvas)
        doc.layers[doc.layers.length - 1].isBackgroundLayer = false;
        // Invoke Photoshop 'Select Subject'
        selectSubject();
        // Get bounds of selection.
        bounds = doc.selection.bounds;
        doc.selection.deselect();
        // Crop image to bounds using resize canvas to preserve pixels outside bounds.
        // Resize canvas to crop out above bounds.
        doc.resizeCanvas(null, doc.height - bounds[1], AnchorPosition.BOTTOMCENTER);
        // Resize canvas to crop out below bounds.
        doc.resizeCanvas(null, bounds[3] - bounds[1], AnchorPosition.TOPCENTER);
        // Resize canvas to crop out left of bounds.
        doc.resizeCanvas(doc.width - bounds[0], null, AnchorPosition.MIDDLERIGHT);
        // Resize canvas to crop out right of bounds.
        doc.resizeCanvas(bounds[2] - bounds[0], null, AnchorPosition.MIDDLELEFT);
        // Resize image to targetWidth x targetHeight less targetMargin doubled.
        resHigh = doc.height / (targetHeight - (targetMargin * 2));
        resWide = doc.width / (targetWidth - (targetMargin * 2));
        resolution = Math.round(Math.max(resHigh, resWide));
        doc.resizeImage(null, null, resolution, ResampleMethod.NONE);
        // Resize canvas to add margin.
        doc.resizeCanvas(targetWidth * resolution, targetHeight * resolution, AnchorPosition.MIDDLECENTER);
        // Check if image fills crop area.
        bounds = doc.layers[0].bounds;
        if (bounds[0] > 0 || bounds[1] > 0 || bounds[2] < doc.width || bounds[3] < doc.height) {
            results.push("ALERT: image does not fill crop area.");
        }
        // Flatten.
        if (cbFlatten.value) {
            doc.flatten();
        }
        // Resample.
        if (rbResolutionResample.value) {
            resolution = Number(inpResolution.text);
            if (resolution > doc.resolution) {
                results.push("ALERT: image resampled from " + doc.resolution + " to " + resolution + " ppi.");
            }
            doc.resizeImage(null, null, resolution, ResampleMethod.AUTOMATIC);
        }
        // Convert to profile.
        if (cbConvert.value) {
            try {
                mergeLayers();
                // Make 8-bit.
                doc.bitsPerChannel = BitsPerChannelType.EIGHT;
                // Convert to profile.
                doc.convertProfile(listProfile.selection.text, Intent.RELATIVECOLORIMETRIC);
            } catch (e) {
                results.push("ALERT: failed to convert to color profile " + listProfile.selection.text);
            }
        }
        return results;
    }

    function progress(steps) {
        var w = new Window("palette", "Progress");
        var b; // bar
        var t = w.add("statictext");
        w.preferredSize = [450, -1];
        if (steps) {
            b = w.add("progressbar", undefined, 0, steps);
            b.preferredSize = [450, -1];
            t.preferredSize = [450, -1];
            w.add("statictext", undefined, "Press ESC to cancel");
        } else {
            t.text = "Working... Please wait...";
        }
        progress.close = function () {
            w.close();
            app.refresh();
        };
        progress.increment = function () {
            b.value++;
        };
        progress.message = function (message) {
            t.text = message;
            w.update();
        };
        w.show();
        app.refresh();
    }

    function rasterizeLayers(o) {
        var i;
        var layer;
        for (i = 0; i < o.layers.length; i++) {
            layer = o.layers[i];
            try {
                layer.rasterize(RasterizeType.ENTIRELAYER);
            } catch (e) {
                // Ignore.
            }
            if (layer.typename == "LayerSet") {
                // If layer set, recurse (call self).
                rasterizeLayers(layer);
            }
        }
    }

    function saveAndClose(doc) {
        var format;
        var name;
        var saveOptions;

        format = listFormat.selection.text.toLowerCase();
        switch (format) {
            case "jpg":
                saveOptions = new JPEGSaveOptions();
                saveOptions.quality = Number(listQuality.selection.text);
                saveOptions.formatOptions = FormatOptions.STANDARDBASELINE;
                saveOptions.embedColorProfile = true;
                break;
            case "png":
                saveOptions = new PNGSaveOptions();
                saveOptions.compression = 0;
                saveOptions.interlaced = false;
                break;
            case "psd":
                saveOptions = new PhotoshopSaveOptions();
                saveOptions.alphaChannels = false;
                saveOptions.annotations = false;
                saveOptions.embedColorProfile = true;
                saveOptions.layers = !cbFlatten.value;
                saveOptions.spotColors = false;
                break;
            case "tif":
                saveOptions = new TiffSaveOptions();
                saveOptions.alphaChannels = false;
                saveOptions.annotations = false;
                saveOptions.embedColorProfile = true;
                saveOptions.layers = !cbFlatten.value;
                saveOptions.spotColors = false;
                saveOptions.imageCompression = TIFFEncoding.TIFFLZW;
                saveOptions.layerCompression = LayerCompression.ZIP;
                break;
            default:
                // None of the above.
                throw new Error("Bad file format.");
        }
        // Ensure output folder exists.
        if (!folderOutput.exists) {
            folderOutput.create();
        }
        // Save.
        name = doc.name.replace(/\.[^\.]*$/, "") + inpSuffix.text + "." + format;
        doc.saveAs(new File(folderOutput + "/" + name), saveOptions);
        doc.close(SaveOptions.DONOTSAVECHANGES);
    }

    function selectSubject() {
        var desc1 = new ActionDescriptor();
        desc1.putBoolean(stringIDToTypeID("sampleAllLayers"), false);
        executeAction(stringIDToTypeID('autoCutout'), desc1, DialogModes.NO);
    }

    function setSuffix() {
        // var inpHeight parent scope.
        // var inpWidth parent scope.
        inpSuffix.text = "_" + inpWidth.text + "x" + inpHeight.text;
    }

    function testCancel() {
        if (ScriptUI.environment.keyboardState.keyName == "Escape") {
            throw new Error("User cancelled");
        }
    }

    function validateNumeric(target, integer) {
        var s;
        var v;
        // Remove non-digits.
        s = target.text.replace(/[^0-9.]/g, "");
        if (s !== target.text) {
            alert("Numeric input only.\nNon-numeric characters removed.");
        }
        // No more than one decimal point.
        s = s.replace(/\.{2,}/g, ".");
        v = parseFloat(s);
        if (integer) {
            v = Math.round(v);
        }
        target.text = v.toString();
        changeOption();
    }

})();

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Most Valuable Participant ,
Aug 26, 2020

Copy link to clipboard

Copied

Wow its large I only hack at scripting I was curious to see if it would remove what select subject  did not select or corp the the selection bounds.. I ran the script several times and I add layers to my test image that did not seem to change the selection that select subject would set.  When I ran the script the crop changed quite a bit I was not expecting it to. 

 

 It would take me forever to read your code. It look like you put a lot of work into it.  However, I do not think I would have any use for a script like it.   I sure hope some will find it useful  and reward you one way or an other.   Thank you for sharing.

 

Many of Adobe Automated features I feel are good starting points like select subject, select object, quick select select  but need to be refined.  And features like content aware need to be used carefully.

 

So I do not think Auto Cropping select subject  for me is that good of an Idea.   If I were to implement something like  this I would not Crop the image  After insuring the bottom layer is a normals layer  I would probable the select all layer and create a Layer group for the document content then mask the grout to the bounds of select subject,  Not actually crop content.

JJMack

Likes

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
Reply
Loading...
Advocate ,
Aug 27, 2020

Copy link to clipboard

Copied

William
a great job
I hope it works for my job

thanks for putting in the sogente code
so I see if I can adapt it to my needs

 

Likes

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
Reply
Loading...
Contributor ,
Aug 27, 2020

Copy link to clipboard

Copied

Thanks. I hope it helps. If you have any questons let me know.

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Contributor ,
Aug 27, 2020

Copy link to clipboard

Copied

Probably you don't really want to see all of the code. Yes it can be overwhelming (and this is a smaller script of mine). There is more to scripts than only the main thing it does, as you can see.

 

Here is the core of "what it does" simplified. This is similar to what I posted in the original discussion, before rolling the code into a full script. See if this code snippet works better for you. Or not. Or maybe it will satisfy your curiosity as to how this thing crops images. At any rate, it's what I imagined as one solution.

 

// Set theses vars as desired (inches).
var targetHeight = 5;
var targetMargin = 0.5;
var targetWidth = 7;
// Other vars.
var doc = app.activeDocument
var bounds;
var desc1;
var resHigh;
var resWide;
var resolution;
// Make bottom layer a layer, not background.
// (to preserve pixels while resizing canvas)
doc.layers[doc.layers.length - 1].isBackgroundLayer = false;
// Invoke Photoshop 'Select Subject'
desc1 = new ActionDescriptor();
desc1.putBoolean(stringIDToTypeID("sampleAllLayers"), false);
executeAction(stringIDToTypeID('autoCutout'), desc1, DialogModes.NO);
// Get bounds of selection.
bounds = doc.selection.bounds;
doc.selection.deselect();
// Crop image to bounds using resize canvas to preserve pixels outside bounds.
// Resize canvas to crop out above bounds.
doc.resizeCanvas(null, doc.height - bounds[1], AnchorPosition.BOTTOMCENTER);
// Resize canvas to crop out below bounds.
doc.resizeCanvas(null, bounds[3] - bounds[1], AnchorPosition.TOPCENTER);
// Resize canvas to crop out left of bounds.
doc.resizeCanvas(doc.width - bounds[0], null, AnchorPosition.MIDDLERIGHT);
// Resize canvas to crop out right of bounds.
doc.resizeCanvas(bounds[2] - bounds[0], null, AnchorPosition.MIDDLELEFT);
// Resize image to targetWidth x targetHeight less targetMargin doubled.
resHigh = doc.height / (targetHeight - (targetMargin * 2));
resWide = doc.width / (targetWidth - (targetMargin * 2));
resolution = Math.round(Math.max(resHigh, resWide));
doc.resizeImage(null, null, resolution, ResampleMethod.NONE);
// Resize canvas to add margin.
doc.resizeCanvas(targetWidth * resolution, targetHeight * resolution, AnchorPosition.MIDDLECENTER);

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Most Valuable Participant ,
Aug 27, 2020

Copy link to clipboard

Copied

I also think of select subject and select object feature more suited for select and extract  not as cropping feature.  Cropping in Photoshop is not a problem.  Cropping to the bounds of a select subject is so unpredictable and resulting composition may what you would want and have an aspect ration that is not good for the canvas you script creates for the users setting. I also did not expect my document Print resolution to bet changed when I user an AS IS option.  I just through a  8" x 12" 292 DPI canvas withe and ovals red shape on top.   Is was not expecting that my Document  8" x 12" 292 DPI document to have a resolution change or that my red shape would be changed to a white shape.

Capture.jpg

 

JJMack

Likes

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
Reply
Loading...
Contributor ,
Aug 27, 2020

Copy link to clipboard

Copied

You are probably right the script is not for you. I didn't envision anyone using it on layered images, and I see a flaw in that scenario already, which I have addressed for v1.1 but haven't posted yet. Soon. Still have to tackle some other layer issues the scenario causes.

 

The script is most appropriate for flattened images of subject matter that lends itself well to Select Subject. Not all subject matter does. Examples in the original discussion that spawned this idea are one type of image that should work well. Other good uses are clients of mine who process thousands of apparel and footwear images shot in a studio, on a consistent background that contrasts well with the product; for them, tools like this (and variations of it) save untold man-hours.

 

About resolution "As-is" I think you're misinterpreting what that means. First it helps to read the documention, which goes into it some. The docs are the web page, and a text file included in the download. Should read all of that. I will elaborate on the resolution section here.

 

"As-is" does not refer to "pixel per inch". It refers to pixels, period. The pixels remain "As-is" rather than any interpolation. If an image is one size, and whether you crop it or not, make the image another size and don't resample it, how on Earth can the "per inch" (or per whatever) value remain constant? It can't. It has to change. That is, the number of pixels that make up an inch has to change. But the pixels are the same pixels you started with, just some are gone based on the desired cropping. That is what "As-is" means, versus "Resample."

 

So of course the "per inch" value will end up different using the "As-is" option. I hope that makes it clearer.

 

About your shape turning white, I have no clue. I made up a similar image and it crops fine, and my red circle stays red. Perhaps a version of Photoshop with ExtendScript glitch? I don't know. I'm running 21.2.2 and I do know this version already has an ExtendScript glitch I'm dealing with, which perhaps is related. When I set foreground color to black in script, it always comes out red (#FF0000). Still looking into what might be up with that.

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Most Valuable Participant ,
Aug 27, 2020

Copy link to clipboard

Copied

My test was very simple it should be easy to replicate. I opened a New 8" x 12" 300 dpi document filled the background with black, created a shape layer and ran  your script.  Your script does not suspend history  so it very easy the step  through what steps your script did.  The very first step your script did was the set the shape layers fill. It then converted the background layer to a normal layer, followed by Select Subject, deselect, Four canvas sizes to crop the four sides.  Image size to change the  Print resolution and a last canvas size to make the canvas 8x12.

Capture.jpg

JJMack

Likes

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
Reply
Loading...
Contributor ,
Aug 27, 2020

Copy link to clipboard

Copied

Ah, shape layer is why. Now my test fails to match yours. However, I think it's bug in ExtendScript. Have to study more (was planning to anyway, for an update to better handle layers). Thing is, nothing in the script is asking to "set shape layer fill". Strange that is appearing in history. It is somehow a by-product of something else, my best guess for the moment. I'll figure out the cause and how to work around it, then post an updated script to my site, probably by tomorrow after some investigation. Thanks.

 

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Contributor ,
Aug 27, 2020

Copy link to clipboard

Copied

It is an ExtendScript bug. Here is short code to demonstrate:

 

var green = new SolidColor();
green.rgb.hexValue = "00FF00";
app.foregroundColor = green;
var blue = new SolidColor();
blue.rgb.hexValue = "0000FF";
app.backgroundColor = blue;

 

Save and run that on your image. Check out what happens. As I suspected earlier, this is related to a bug I've already encountered. Notice the code above is setting foreground to green. Nope, it becomes Red. No matter what color it's set to, foreground become Red instead. And of course, the flaw you uncovered, the shape layer becomes blue, which was intended for the application background color, NOT the shape layer's color. Something is very wrong under the hood of this version.

 

Tomorrow I will try an earlier verison of Photoshop and see what I learn.

 

Edit, minutes later: Well, something is nuts. Now green works fine. But still the shape is turning blue. Will be working on it.

 

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...
Most Valuable Participant ,
Aug 27, 2020

Copy link to clipboard

Copied

Yes there are bugs in Photoshop scriptingsupport users have to work around.

JJMack

Likes

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
Reply
Loading...
Contributor ,
Aug 28, 2020

Copy link to clipboard

Copied

Script has been revised to work around ExtendScript bug coloring shape layer when setting app.backgroundColor. That is fixed and new option added to resample to original resolution after cropping. Also better UI verbiage for As-is option, hopefully make it clear what that means. Version 1.1 is available to download. 

https://www.marspremedia.com/software/photoshop/auto-crop

Thanks all,

 

William Campbell
Mars Premedia

Likes

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
Reply
Loading...