/
index.js
184 lines (154 loc) · 4.77 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// constants
const ASE_SIGNATURE = 0x41534546;
const ASE_VERSION_MAJOR = 1;
const ASE_VERSION_MINOR = 0;
const ASE_BLOCK_TYPE_COLOR = 0x1;
const ASE_BLOCK_TYPE_GROUP_START = 0xC001;
const ASE_BLOCK_TYPE_GROUP_END = 0xC002;
const ASE_COLOR_TYPES = ['global', 'spot', 'normal'];
// get an object from an ase buffer
module.exports = (buffer) => {
assert(Buffer.isBuffer(buffer), 'The argument is not an instance of Buffer', TypeError);
assert(buffer.readUInt32BE(0) === ASE_SIGNATURE, 'Invalid file signature: ASEF header expected');
assert(buffer.readUInt16BE(4) === ASE_VERSION_MAJOR, 'Only version 1.0 of the ASE format is supported');
assert(buffer.readUInt16BE(6) === ASE_VERSION_MINOR, 'Only version 1.0 of the ASE format is supported');
return readBlocks(buffer, 12).reduce(reduceGroups, []);
};
// reads blocks from a buffer
const readBlocks = (buffer, offset) => {
const result = [];
let length = buffer.readUInt32BE(8);
let readOffset = offset;
while (length--) {
readOffset += readBlock(buffer, readOffset, result);
}
return result;
};
// reads a block from a buffer
const readBlock = (buffer, offset, result) => {
const type = buffer.readUInt16BE(offset);
switch (type) {
case ASE_BLOCK_TYPE_COLOR:
result.push(readColorEntry(buffer, offset + 6));
break;
case ASE_BLOCK_TYPE_GROUP_START:
result.push(readGroupStart(buffer, offset + 6));
break;
case ASE_BLOCK_TYPE_GROUP_END:
result.push({
type: 'group-end'
});
break;
default:
throw new Error('Unsupported type ' + type.toString(16) + ' at offset ' + offset);
}
return 6 + buffer.readUInt32BE(offset + 2);
};
// reads a group start from a buffer
const readGroupStart = (buffer, offset) => {
const nameLength = buffer.readUInt16BE(offset);
return {
type: 'group-start',
name: bufferToUTF16(buffer, offset + 2, nameLength).trim()
};
};
// reads a color entry from a buffer
const readColorEntry = (buffer, offset) => {
var nameLength = buffer.readUInt16BE(offset);
return {
type: 'color',
name: bufferToUTF16(buffer, offset + 2, nameLength).trim(),
color: readColor(buffer, offset + 2 + nameLength * 2)
};
};
// reads a color from a buffer
const readColor = (buffer, offset) => {
const model = buffer.toString('utf8', offset, offset + 4).trim();
switch (model) {
case 'RGB':
return {
model: model,
r: Math.round(buffer.readFloatBE(offset + 4) * 255),
g: Math.round(buffer.readFloatBE(offset + 8) * 255),
b: Math.round(buffer.readFloatBE(offset + 12) * 255),
type: ASE_COLOR_TYPES[buffer.readUInt16BE(offset + 16)]
};
case 'CMYK':
return {
model: model,
c: Math.round(buffer.readFloatBE(offset + 4) * 100),
m: Math.round(buffer.readFloatBE(offset + 8) * 100),
y: Math.round(buffer.readFloatBE(offset + 12) * 100),
k: Math.round(buffer.readFloatBE(offset + 16) * 100),
type: ASE_COLOR_TYPES[buffer.readUInt16BE(offset + 20)]
};
case 'Gray':
return {
model: model,
gray: buffer.readFloatBE(offset + 4),
type: ASE_COLOR_TYPES[buffer.readUInt16BE(offset + 8)]
};
case 'LAB':
return {
model: model,
lightness: buffer.readFloatBE(offset + 4),
a: buffer.readFloatBE(offset + 8),
b: buffer.readFloatBE(offset + 12),
type: ASE_COLOR_TYPES[buffer.readUInt16BE(offset + 16)]
};
default:
throw new Error('Unsupported color model: ' + model + ' at offset ' + offset);
}
};
// converts a buffer to utf-16
const bufferToUTF16 = (buffer, position, nameLength) => {
let name = '';
let index = 0;
while (index < nameLength - 1) {
name += String.fromCharCode(
buffer.readUInt16BE(position + index * 2)
);
index += 1;
}
return name;
};
// asserts a condition or throws an error
const assert = (condition, message, ErrorType = Error) => {
if (!condition) {
throw new ErrorType(message);
}
};
// reduces groups into object keys
const reduceGroups = (accumulated, item) => {
const last = accumulated[accumulated.length - 1];
if (last && last.type === 'group-start') {
if (item.type === 'group-end') {
last.type = 'group';
} else {
if (item.color.model === 'RGB') {
item.color.hex = rgbToHex(item.color);
} else if (item.color.model === 'CMYK') {
item.color.hex = cmykToHex(item.color);
}
last.entries.push(item);
}
} else if (item.type === 'group-start') {
item.entries = [];
accumulated.push(item);
} else {
accumulated.push(item);
}
return accumulated;
};
// converts rgb to hex
const rgbToHex = ({ r, g, b }) => {
return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`;
};
// converts cmyk to hex (without a color profile)
const cmykToHex = ({ c, m, y, k }) => {
return rgbToHex({
r: Math.round(255 * (1 - c / 100) * (1 - k / 100)),
g: Math.round(255 * (1 - m / 100) * (1 - k / 100)),
b: Math.round(255 * (1 - y / 100) * (1 - k / 100))
});
};