1 | const zlib = require('zlib');
|
2 |
|
3 | class Aseprite {
|
4 | constructor(buffer, name) {
|
5 | this._offset = 0;
|
6 | this._buffer = buffer;
|
7 | this.frames = [];
|
8 | this.layers = [];
|
9 | this.slices = [];
|
10 | this.fileSize;
|
11 | this.numFrames;
|
12 | this.width;
|
13 | this.height;
|
14 | this.colorDepth;
|
15 | this.paletteIndex;
|
16 | this.numColors;
|
17 | this.pixelRatio;
|
18 | this.name = name;
|
19 | this.tags = [];
|
20 | }
|
21 | readNextByte() {
|
22 | const nextByte = this._buffer.readUInt8(this._offset);
|
23 | this._offset += 1;
|
24 | return nextByte;
|
25 | }
|
26 | readByte(offset) {
|
27 | return this._buffer.readUInt8(offset);
|
28 | }
|
29 | readNextWord() {
|
30 | const word = this._buffer.readUInt16LE(this._offset);
|
31 | this._offset += 2;
|
32 | return word;
|
33 | }
|
34 | readWord(offset) {
|
35 | return this._buffer.readUInt16LE(offset);
|
36 | }
|
37 | readNextShort() {
|
38 | const short = this._buffer.readInt16LE(this._offset);
|
39 | this._offset += 2;
|
40 | return short;
|
41 | }
|
42 | readShort(offset) {
|
43 | return this._buffer.readInt16LE(offset);
|
44 | }
|
45 | readNextDWord() {
|
46 | const dWord = this._buffer.readUInt32LE(this._offset);
|
47 | this._offset += 4;
|
48 | return dWord;
|
49 | }
|
50 | readDWord(offset) {
|
51 | return this._buffer.readUInt32LE(offset);
|
52 | }
|
53 | readNextLong() {
|
54 | const long = this._buffer.readInt32LE(this._offset);
|
55 | this._offset += 4;
|
56 | return long;
|
57 | }
|
58 | readLong(offset) {
|
59 | return this._buffer.readInt32LE(offset);
|
60 | }
|
61 | readNextFixed() {
|
62 | const fixed = this._buffer.readFloatLE(this._offset);
|
63 | this._offset += 4;
|
64 | return fixed;
|
65 | }
|
66 | readFixed(offset) {
|
67 | return this._buffer.readFloatLE(offset);
|
68 | }
|
69 | readNextBytes(numBytes) {
|
70 | let strBuff = Buffer.alloc(numBytes);
|
71 | for (let i = 0; i < numBytes; i++) {
|
72 | strBuff.writeUInt8(this.readNextByte(), i);
|
73 | }
|
74 | return strBuff.toString();
|
75 | }
|
76 | readNextRawBytes(numBytes) {
|
77 | let buff = Buffer.alloc(numBytes);
|
78 | for (let i = 0; i < numBytes; i++) {
|
79 | buff.writeUInt8(this.readNextByte(), i);
|
80 | }
|
81 | return buff;
|
82 | }
|
83 |
|
84 | readRawBytes(numBytes, b, offset) {
|
85 | let buff = Buffer.alloc(numBytes - offset);
|
86 | for (let i = 0; i < numBytes - offset; i++) {
|
87 | buff.writeUInt8(b.readUInt8(offset + i), i);
|
88 | }
|
89 | return buff;
|
90 | }
|
91 | readNextString() {
|
92 | const numBytes = this.readNextWord();
|
93 | return this.readNextBytes(numBytes);
|
94 | }
|
95 | skipBytes(numBytes) {
|
96 | this._offset += numBytes;
|
97 | }
|
98 | readHeader() {
|
99 | this.fileSize = this.readNextDWord();
|
100 | this.readNextWord();
|
101 | this.numFrames = this.readNextWord();
|
102 | this.width = this.readNextWord();
|
103 | this.height = this.readNextWord();
|
104 | this.colorDepth = this.readNextWord();
|
105 | this.skipBytes(14);
|
106 | this.paletteIndex = this.readNextByte();
|
107 | this.skipBytes(3);
|
108 | this.numColors = this.readNextWord();
|
109 | const pixW = this.readNextByte();
|
110 | const pixH = this.readNextByte();
|
111 | this.pixelRatio = `${pixW}:${pixH}`;
|
112 | this.skipBytes(92);
|
113 | return this.numFrames;
|
114 | }
|
115 | readFrame() {
|
116 | const bytesInFrame = this.readNextDWord();
|
117 | this.skipBytes(2);
|
118 | const oldChunk = this.readNextWord();
|
119 | const frameDuration = this.readNextWord();
|
120 | this.skipBytes(2);
|
121 | const newChunk = this.readNextDWord();
|
122 | let cels = [];
|
123 | for(let i = 0; i < newChunk; i ++) {
|
124 | let chunkData = this.readChunk();
|
125 | switch(chunkData.type) {
|
126 | case 0x0004:
|
127 | case 0x0011:
|
128 | case 0x2016:
|
129 | case 0x2017:
|
130 | case 0x2020:
|
131 | this.skipBytes(chunkData.chunkSize - 6);
|
132 | break;
|
133 | case 0x2022:
|
134 | this.readSliceChunk();
|
135 | break;
|
136 | case 0x2004:
|
137 | this.readLayerChunk();
|
138 | break;
|
139 | case 0x2005:
|
140 | let celData = this.readCelChunk(chunkData.chunkSize);
|
141 | cels.push(celData);
|
142 | break;
|
143 | case 0x2007:
|
144 | this.readColorProfileChunk();
|
145 | break;
|
146 | case 0x2018:
|
147 | this.readFrameTagsChunk();
|
148 | break;
|
149 | case 0x2019:
|
150 | this.palette = this.readPaletteChunk();
|
151 | break;
|
152 | }
|
153 | }
|
154 | this.frames.push({ bytesInFrame,
|
155 | frameDuration,
|
156 | numChunks: newChunk,
|
157 | cels});
|
158 | }
|
159 | readColorProfileChunk() {
|
160 | const types = [
|
161 | 'None',
|
162 | 'sRGB',
|
163 | 'ICC'
|
164 | ]
|
165 | const typeInd = this.readNextWord();
|
166 | const type = types[typeInd];
|
167 | const flag = this.readNextWord();
|
168 | const fGamma = this.readNextFixed();
|
169 | this.skipBytes(8);
|
170 |
|
171 | this.colorProfile = {
|
172 | type,
|
173 | flag,
|
174 | fGamma};
|
175 | }
|
176 | readFrameTagsChunk() {
|
177 | const loops = [
|
178 | 'Forward',
|
179 | 'Reverse',
|
180 | 'Ping-pong'
|
181 | ]
|
182 | const numTags = this.readNextWord();
|
183 | this.skipBytes(8);
|
184 | for(let i = 0; i < numTags; i ++) {
|
185 | let tag = {};
|
186 | tag.from = this.readNextWord();
|
187 | tag.to = this.readNextWord();
|
188 | const loopsInd = this.readNextByte();
|
189 | tag.animDirection = loops[loopsInd];
|
190 | this.skipBytes(8);
|
191 | tag.color = this.readNextRawBytes(3).toString('hex');
|
192 | this.skipBytes(1);
|
193 | tag.name = this.readNextString();
|
194 | this.tags.push(tag);
|
195 | }
|
196 | }
|
197 | readPaletteChunk() {
|
198 | const paletteSize = this.readNextDWord();
|
199 | const firstColor = this.readNextDWord();
|
200 | const secondColor = this.readNextDWord();
|
201 | this.skipBytes(8);
|
202 | let colors = [];
|
203 | for (let i = 0; i < paletteSize; i++) {
|
204 | let flag = this.readNextWord();
|
205 | let red = this.readNextByte();
|
206 | let green = this.readNextByte();
|
207 | let blue = this.readNextByte();
|
208 | let alpha = this.readNextByte();
|
209 | let name;
|
210 | if (flag === 1) {
|
211 | name = this.readNextString();
|
212 | }
|
213 | colors.push({
|
214 | red,
|
215 | green,
|
216 | blue,
|
217 | alpha,
|
218 | name: name !== undefined ? name : "none"
|
219 | });
|
220 | }
|
221 | let palette = {
|
222 | paletteSize,
|
223 | firstColor,
|
224 | lastColor: secondColor,
|
225 | colors
|
226 | }
|
227 | this.colorDepth === 8 ? palette.index = this.paletteIndex : '';
|
228 | return palette;
|
229 | }
|
230 | readSliceChunk() {
|
231 | const numSliceKeys = this.readNextDWord();
|
232 | const flags = this.readNextDWord();
|
233 | this.skipBytes(4);
|
234 | const name = this.readNextString();
|
235 | const keys = [];
|
236 | for(let i = 0; i < numSliceKeys; i ++) {
|
237 | const frameNumber = this.readNextDWord();
|
238 | const x = this.readNextLong();
|
239 | const y = this.readNextLong();
|
240 | const width = this.readNextDWord();
|
241 | const height = this.readNextDWord();
|
242 | const key = { frameNumber, x, y, width, height };
|
243 | if((flags & 2) !== 0) {
|
244 | key.patch = this.readSlicePatchChunk();
|
245 | key.pivot = this.readSlicePivotChunk();
|
246 | }
|
247 | keys.push(key);
|
248 | }
|
249 | this.slices.push({ flags, name, keys });
|
250 | }
|
251 | readSlicePatchChunk() {
|
252 | const x = this.readNextLong();
|
253 | const y = this.readNextLong();
|
254 | const width = this.readNextDWord();
|
255 | const height = this.readNextDWord();
|
256 | return { x, y, width, height };
|
257 | }
|
258 | readSlicePivotChunk() {
|
259 | const x = this.readNextLong();
|
260 | const y = this.readNextLong();
|
261 | return { x, y };
|
262 | }
|
263 | readLayerChunk() {
|
264 | const flags = this.readNextWord();
|
265 | const type = this.readNextWord();
|
266 | const layerChildLevel = this.readNextWord();
|
267 | this.skipBytes(4);
|
268 | const blendMode = this.readNextWord();
|
269 | const opacity = this.readNextByte();
|
270 | this.skipBytes(3);
|
271 | const name = this.readNextString();
|
272 | this.layers.push({ flags,
|
273 | type,
|
274 | layerChildLevel,
|
275 | blendMode,
|
276 | opacity,
|
277 | name});
|
278 | }
|
279 |
|
280 | readCelChunk(chunkSize) {
|
281 | const layerIndex = this.readNextWord();
|
282 | const x = this.readNextShort();
|
283 | const y = this.readNextShort();
|
284 | const opacity = this.readNextByte();
|
285 | const celType = this.readNextWord();
|
286 | this.skipBytes(7);
|
287 | const w = this.readNextWord();
|
288 | const h = this.readNextWord();
|
289 | const buff = this.readNextRawBytes(chunkSize - 26);
|
290 | let rawCel;
|
291 | if(celType === 2) {
|
292 | rawCel = zlib.inflateSync(buff);
|
293 | } else if(celType === 0) {
|
294 | rawCel = buff;
|
295 | }
|
296 | return { layerIndex,
|
297 | xpos: x,
|
298 | ypos: y,
|
299 | opacity,
|
300 | celType,
|
301 | w,
|
302 | h,
|
303 | rawCelData: rawCel}
|
304 | }
|
305 | readChunk() {
|
306 | const cSize = this.readNextDWord();
|
307 | const type = this.readNextWord();
|
308 | return {chunkSize: cSize, type: type};
|
309 | }
|
310 | parse() {
|
311 | const numFrames = this.readHeader();
|
312 | for(let i = 0; i < numFrames; i ++) {
|
313 | this.readFrame();
|
314 | }
|
315 |
|
316 | }
|
317 | formatBytes(bytes,decimals) {
|
318 | if (bytes === 0) {
|
319 | return '0 Byte';
|
320 | }
|
321 | const k = 1024;
|
322 | const dm = decimals + 1 || 3;
|
323 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
324 | const i = Math.floor(Math.log(bytes) / Math.log(k));
|
325 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
326 | };
|
327 | toJSON() {
|
328 | return {
|
329 | fileSize: this.fileSize,
|
330 | numFrames: this.numFrames,
|
331 | frames: this.frames.map(frame => {
|
332 | return {
|
333 | size: frame.bytesInFrame,
|
334 | duration: frame.frameDuration,
|
335 | chunks: frame.numChunks,
|
336 | cels: frame.cels.map(cel => {
|
337 | return {
|
338 | layerIndex: cel.layerIndex,
|
339 | xpos: cel.xpos,
|
340 | ypos: cel.ypos,
|
341 | opacity: cel.opacity,
|
342 | celType: cel.celType,
|
343 | w: cel.w,
|
344 | h: cel.h,
|
345 | rawCelData: 'buffer'
|
346 | }
|
347 | }) }
|
348 | }),
|
349 | palette: this.palette,
|
350 | width: this.width,
|
351 | height: this.height,
|
352 | colorDepth: this.colorDepth,
|
353 | numColors: this.numColors,
|
354 | pixelRatio: this.pixelRatio,
|
355 | layers: this.layers,
|
356 | slices: this.slices
|
357 | };
|
358 | }
|
359 | }
|
360 |
|
361 | module.exports = Aseprite;
|