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();
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'app.activeDocument.colorSamplers.add([x,y])
.