0

In ExtendScript for Photoshop is there a way to read the swatches like name-color and the configuration of a text tool preset? I want to detect the color from some rectangles in the image, get the name of the swatch associated to and write a text inside it with the style of the preset with the same name.

The best would be being able to read them from a .aco file for the swatches and from a .tpl file for the tool presets but it might be fine even read them from inside Photoshop. I looked at the official documentation by I couldn't find anything about it. I tried to reverse engineer an aco file and all I found is that it is an hex file with the name of the swatches encoded in utf16-be but I couldn't find the correspondent color. Even the tpl file has strings encoded in utf8. Thanks.

4
  • Those are separate questions. Yes, you can read current tool options via Action Manager currentToolOptions descriptor. Yes, you can read .aco and .tpl: there's files specification. Not sure I understand how you want to 'detect color from rectangles' Aug 12, 2020 at 18:59
  • Ops, my bad. However, you gave me some ideas with that link. Thank you. About the color, the rectangles are each in a layer by themselves so I can pick the color from the pixels in the center with app.activeDocument.colorSamplers.add([x,y]).
    – Ripper346
    Aug 12, 2020 at 22:40
  • @SergeyKritskiy thanks to the document you linked I was able to decode aco files but I have some troubles with tpl ones, do you know if there is a document about how are organized the preset tools, in particular the typeCreateOrEditTool? At the moment I am looking to the atn specification to extract something but it isn't sufficient
    – Ripper346
    Aug 20, 2020 at 9:13
  • no sorry :( I myself can't read binaries: I just know they exist and some people can read those and there are ones and zeroes and that's it :) Aug 20, 2020 at 9:59

1 Answer 1

0

After a lot of troubles I came up with a decoder for swatches files (.aco) and tool preset (.tpl). Many thanks to @SergeyKritskiy, without you I couldn't have had a guide to start with. I tested aco decode and tpl decode of text and rectangle tools.

The first thing I made (not really, it was along the way) was an utility file with some useful function, ECMA 3 it's so ancient... I think that readUntilHexPlaceholder method could be refactored but I don't feel like to do it, even because in theory it shouldn't be used by the code, it's there for more security in case of length read errors.

utility.jsx

function stringToHex(text, encodeLen) {
    var hex = "";
    for (var i = 0; i < text.length; i++) {
        var conv = '00000000' + text.charCodeAt(i).toString(16);
        hex += conv.substr(conv.length - encodeLen, encodeLen);
    }
    return hex;
}

function hexToString(hexString, decodeLen) {
    var text = "";
    for (var i = 0; i < hexString.length / decodeLen; i++) {
        text += String.fromCharCode(parseInt(hexString.substr(i * decodeLen, decodeLen), 16));
    }
    return text;
}

/**
 * Convert hex representation of a IEE754 standard double floating point number to a JS number
 */
function IEE75464BitToNumber(hex) {
    var binary = ('0000000000000000000000000000000000000000000000000000000000000000' +
        parseInt(hex, 16).toString(2)).substr(-64, 64);
    var sign = binary[0] == 0 ? 1 : -1;
    var exponent = parseInt(binary.substr(1, 11), 2);
    var mantissa = binary.substr(12, 52);

    if (exponent == 0 && parseInt(mantissa, 2) == 0) return 0.0;
    if (exponent == 0x7FF && parseInt(mantissa, 2) == 0) return sign * Infinity;
    if (exponent == 0x7FF && parseInt(mantissa, 2) != 0) return NaN;

    var fraction = exponent == 0 && parseInt(mantissa, 2) != 0 ? 0.0 : 1.0;
    for (var i = 1; i < 53; i++)
        fraction += Number(mantissa[i - 1]) * Math.pow(2, -i);
    var doubleNumber = sign * fraction * Math.pow(2, exponent - 1023);
    var roundCeil = doubleNumber.toString().match(/(((\.)(9{5,}|0{5,}))|((\.\d*[^9]+)9{5,})|((\.\d*[^0]+)0{5,}))(\d{1-3})?/);
    if (roundCeil) {
        var precision =
            (roundCeil[3] ? roundCeil[3].length : 0) +
            (roundCeil[6] ? roundCeil[6].length : 0) +
            (roundCeil[8] ? roundCeil[8].length : 0) - 1;
        doubleNumber = Math.round(doubleNumber * Math.pow(10, precision)) / Math.pow(10, precision);
    }
    return doubleNumber;
}

function readUntilHexPlaceholder(file, placeholder, bytes, prefix) {
    var text = prefix ? prefix : '';
    while (true) {
        text += file.read(1);
        switch (placeholder.type) {
            case 'hex':
                if (stringToHex(text, bytes).indexOf(placeholder.content) >= 0) {
                    return [
                        text.substr(0, text.length - placeholder.content.length / bytes),
                        text.substr(-placeholder.content.length / bytes, text.length)
                    ];
                }
                break;
            case 'text':
                if (text.indexOf(placeholder.content) >= 0) {
                    return [
                        text.substr(0, text.length - placeholder.content.length),
                        text.substr(-placeholder.content.length, text.length)
                    ];
                }
                break;
            case 'hexArray':
                if (arrayIndexOf(placeholder.content, stringToHex(text, bytes).substr(-placeholder.content[0].length, placeholder.content[0].length)) >= 0) {
                    return [
                        text.substr(0, text.length - placeholder.content[0].length / bytes),
                        text.substr(-placeholder.content[0].length / bytes, text.length)
                    ];
                }
                break;
            case 'textArray':
                if (arrayIndexOf(placeholder.content, text.substr(-placeholder.content[0].length, placeholder.content[0].length)) >= 0) {
                    return [
                        text.substr(0, text.length - placeholder.content[0].length),
                        text.substr(-placeholder.content[0].length, text.length)
                    ];
                }
                break;
        }
    }
    return text;
}

function arrayIndexOf(arr, elem) {
    for (var i = 0; i < arr.length; i++)
        if (arr[i] == elem)
            return i;
    return -1;
}

Swatches reader

This file needs to be improved to convert other color standards, by now it decodes RGB and it converts HSV to RGB. They are the only two that I may use, so for decoding others you have to look at the guide linked on top at the section color (CMYK, LAB, grayscale and customs) and find the conversion methods on the web.

acoReader.jsx

//@include "utility.jsx"

function readAco(path) {
    var aco = {};
    var file = path instanceof File ? path : new File(path);
    file.open('r');
    file.encoding = 'binary';
    try {
        aco[1] = readVersion1(file);
        aco[2] = readVersion2(file);
    } catch (error) {} finally {
        file.close();
    }
    return aco;
}

function readVersion1(file) {
    file.read(2);
    var count = parseInt(stringToHex(file.read(2), 2), 16);
    var colors = [];
    for (var i = 0; i < count; i++) {
        colors.push({
            color: convertColor(parseInt(stringToHex(file.read(2), 2), 16), stringToHex(file.read(8), 2))
        });
    }
    return {
        count: count,
        colors: colors
    };
}

function readVersion2(file) {
    file.read(2);
    var count = parseInt(stringToHex(file.read(2), 2), 16);
    var colors = [];
    for (var i = 0; i < count; i++) {
        var color = convertColor(parseInt(stringToHex(file.read(2), 2), 16), stringToHex(file.read(8), 2));
        var name = processText(file);
        colors.push({
            color: color,
            name: name
        });
    }
    return {
        count: count,
        colors: colors
    };
}

function processText(file) {
    var length = parseInt(stringToHex(file.read(4), 2), 16);
    return hexToString(stringToHex(file.read(length * 2), 2), 4).substr(0, length - 1);
}

function convertColor(format, color) {
    switch (format) {
        case 0:
            return {
                r: 256 / 65536 * parseInt(color.substr(0, 4), 16),
                g: 256 / 65536 * parseInt(color.substr(4, 4), 16),
                b: 256 / 65536 * parseInt(color.substr(8, 4), 16)
            }
        case 1:
            var hue = 1 / 65536 * parseInt(color.substr(0, 4), 16);
            var sat = 1 / 65536 * parseInt(color.substr(4, 4), 16);
            var bri = 1 / 65536 * parseInt(color.substr(8, 4), 16);
            return HSVtoRGB(hue, sat, bri);
        default:
            break;
    }
}

function HSVtoRGB(h, s, v) {
    var r, g, b, i, f, p, q, t;
    if (arguments.length === 1) {
        s = h.s, v = h.v, h = h.h;
    }
    i = Math.floor(h * 6);
    f = h * 6 - i;
    p = v * (1 - s);
    q = v * (1 - f * s);
    t = v * (1 - (1 - f) * s);
    switch (i % 6) {
        case 0:
            r = v, g = t, b = p;
            break;
        case 1:
            r = q, g = v, b = p;
            break;
        case 2:
            r = p, g = v, b = t;
            break;
        case 3:
            r = p, g = q, b = v;
            break;
        case 4:
            r = t, g = p, b = v;
            break;
        case 5:
            r = v, g = p, b = q;
            break;
    }
    return {
        r: Math.round(r * 255),
        g: Math.round(g * 255),
        b: Math.round(b * 255)
    }
}

Tool preset reader

Swatches was quite easy to read, I had most of troubles with tpl files. The osType decoding is incomplete as I didn't find all the types: type and GlbC are defined but not tested, obj , alis, tdata don't have anything. Note that for tool presets I used as reference the action section of the guide, so they couldn't exist in tpl files as there are decoding differences of the osTypes between actions and tools. The script decodes the file, it doesn't give a meaning for all the options (ex: text tool has more than 120 parameters) if you have like Sz as parameter, it's more likely that it's the size of the text and not the font, think about the names you could have.

tplReader.jsx

//@include "utility.jsx"

function readTpl(path) {
    var tpl = {};
    var file = path instanceof File ? path : new File(path);
    file.open('r');
    file.encoding = 'binary';
    try {
        if (!readInit(file))
            return tpl;
        var count = parseInt(stringToHex(file.read(4), 2), 16);
        for (var i = 0; i < count; i++) {
            var tool = readTool(file);
            tpl[tool.name] = tool;
        }
    } catch (error) {
        alert(error.message);
    } finally {
        file.close();
    }
    return tpl;
}

function readInit(file) {
    var incipit = file.read(4);
    if (incipit.toLowerCase() !== '8btp')
        return false;
    file.read(8);
    incipit = file.read(4);
    if (incipit.toLowerCase() !== '8bim')
        return false;
    file.read(8);
    return true;
}

function readTool(file) {
    var name = processText(file);
    file.read(4);
    file.read(4);
    file.read(2);
    var tool = readLabel(file);
    var count = parseInt(stringToHex(file.read(4), 2), 16);
    var properties = [];
    for (var i = 0; i < count; i++) {
        properties.push(readProperty(file));
    }
    return {
        name: name,
        tool: tool,
        properties: properties
    };
}

function readProperty(file) {
    var name = readLabel(file);
    if (name !== 'null')
        return readPropertyValue(file, name);
    return {};
}

function readPropertyValue(file, name) {
    var osType = file.read(4);
    var prop = readOsType(file, name, osType);
    var property = {};
    property[prop.name] = {
        type: prop.osType,
        value: prop.value
    };
    return property;
}

function readLabel(file) {
    var length = parseInt(stringToHex(file.read(4), 2), 16);
    if (length === 0)
        // null
        return file.read(4);
    return file.read(length);
}

function readOsType(file, name, osType) {
    switch (osType) {
        case 'GlbO': // GlobalObject same as Descriptor
        case 'Objc': // Descriptor
            return {
                name: name,
                osType: osType,
                value: processObjc(file)
            };
        case 'VlLs': // List
            return {
                name: name,
                osType: osType,
                value: processList(file)
            };
        case 'doub': // Double
            return {
                name: name,
                osType: osType,
                value: processDouble(file)
            };
        case 'UntF': // Unit float
            return {
                name: name,
                osType: osType,
                value: processUnitFloat(file)
            };
        case 'TEXT': // String
            return {
                name: name,
                osType: osType,
                value: processText(file)
            };
        case 'enum': // Enumerated
            return {
                name: name,
                osType: osType,
                value: processEnum(file)
            };
        case 'long': // Integer
            return {
                name: name,
                osType: osType,
                value: processInteger(file)
            };
        case 'comp': // Large Integer
            return {
                name: name,
                osType: osType,
                value: processLargeInteger(file)
            };
        case 'bool': // Boolean
            return {
                name: name,
                osType: osType,
                value: processBool(file)
            };
        case 'type': // Class
        case 'GlbC': // Class
            // return {name: name, osType: osType, value: processClass(file)};
        case 'obj ': // Reference
        case 'alis': // Alias
        case 'tdta': // Raw Data
            break;
        default:
            // Not recognized, possibly read short name
            var nameFound = readUntilHexPlaceholder(file, {
                type: 'textArray',
                content: ['GlbO', 'Objc', 'VlLs', 'doub', 'UntF', 'TEXT', 'enum', 'long', 'comp', 'bool', 'type', 'GlbC', 'obj ', 'alis', 'tdta']
            }, 2, name + osType);
            name = nameFound[0];
            osType = nameFound[1];
            return {
                name: name,
                osType: osType,
                value: readOsType(file, name, osType)
            };
    }
}

function processText(file) {
    var length = parseInt(stringToHex(file.read(4), 2), 16);
    return hexToString(stringToHex(file.read(length * 2), 2), 4).substr(0, length - 1);
}

function processObjc(file) {
    file.read(4);
    file.read(2);
    readLabel(file);
    var count = parseInt(stringToHex(file.read(4), 2), 16);
    var properties = [];
    for (var i = 0; i < count; i++) {
        properties.push(readProperty(file));
    }
    return properties;
}

function processBool(file) {
    return 1 == parseInt(stringToHex(file.read(1), 2), 16);
}

function processEnum(file) {
    var length = parseInt(stringToHex(file.read(4), 2), 16);
    if (length === 0) length = 4;
    var classId = file.read(length);
    length = parseInt(stringToHex(file.read(4), 2), 16);
    if (length === 0) length = 4;
    var value = file.read(length);
    return {
        classId: classId,
        value: value
    };
}

function processDouble(file) {
    return IEE75464BitToNumber(stringToHex(file.read(8), 2));
}

function processUnitFloat(file) {
    return {
        unit: file.read(4),
        value: IEE75464BitToNumber(stringToHex(file.read(8), 2))
    };
}

function processInteger(file) {
    return parseInt(stringToHex(file.read(4), 2), 16);
}

function processLargeInteger(file) {
    return parseInt(stringToHex(file.read(8), 2), 16);
}

function processList(file) {
    var count = parseInt(stringToHex(file.read(4), 2), 16);
    var properties = [];
    for (var i = 0; i < count; i++) {
        properties.push(readPropertyValue(file, i)[i]);
    }
    return properties;
}

function processClass(file) {
    return readLabel(file);
}

How to test them

//@include "acoReader.jsx"
//@include "tplReader.jsx"
// JSON functions, read the answer and download the file linked at https://stackoverflow.com/a/56391294/8160995
//@include "json2.js";

For swatches you could just make this method:

function getSwatches() {
    var aco = new File('~');
    aco = aco.openDlg(undefined, "Swatches:*.ACO,All files:*.*");
    if (!aco)
        return;
    var acoSet = readAco(aco);
    alert(JSON.stringify(acoSet));
    return acoSet;
}

getSwatches();

And similarly with tool preset:

function getToolPreset() {
    var tpl = new File('~');
    tpl = tpl.openDlg(undefined, "Tool Presets:*.TPL,All files:*.*");
    if (!tpl)
        return;
    var tplTools = readTpl(tpl);
    alert(JSON.stringify(tplTools));
    return tplTools;
}

getToolPreset();
2
  • If your running photoshop why dont you do color conversions in photoshop? I mean half of the cost of the software is the color engine. Also while reading aco outside photoshop is fine i dont really see the point in that as you can just read the swatches loaded in photoshop with far less work.
    – joojaa
    Aug 23, 2020 at 16:14
  • @joojaa which are the color conversion methods you mean? I didn't find any in the reference. To answering your second thought, I can't read the swatches inside photoshop because there could be duplicates in different groups and also for what I am going to do it's not important that the swatches and tools to be loaded inside photoshop when running the script
    – Ripper346
    Aug 23, 2020 at 18:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.