1 | const zlib = require('zlib');
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | class Aseprite {
|
7 | constructor(buffer, name) {
|
8 | this._offset = 0;
|
9 | this._buffer = buffer;
|
10 | this.frames = [];
|
11 | this.layers = [];
|
12 | this.slices = [];
|
13 | this.fileSize;
|
14 | this.numFrames;
|
15 | this.width;
|
16 | this.height;
|
17 | this.colorDepth;
|
18 | this.paletteIndex;
|
19 | this.numColors;
|
20 | this.pixelRatio;
|
21 | this.name = name;
|
22 | this.tags = [];
|
23 | this.tilesets = [];
|
24 | }
|
25 |
|
26 | |
27 |
|
28 |
|
29 |
|
30 |
|
31 | readNextByte() {
|
32 | const nextByte = this._buffer.readUInt8(this._offset);
|
33 | this._offset += 1;
|
34 | return nextByte;
|
35 | }
|
36 |
|
37 | |
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | readByte(offset) {
|
44 | return this._buffer.readUInt8(offset);
|
45 | }
|
46 |
|
47 | |
48 |
|
49 |
|
50 |
|
51 |
|
52 | readNextWord() {
|
53 | const word = this._buffer.readUInt16LE(this._offset);
|
54 | this._offset += 2;
|
55 | return word;
|
56 | }
|
57 |
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | readWord(offset) {
|
65 | return this._buffer.readUInt16LE(offset);
|
66 | }
|
67 |
|
68 | |
69 |
|
70 |
|
71 |
|
72 |
|
73 | readNextShort() {
|
74 | const short = this._buffer.readInt16LE(this._offset);
|
75 | this._offset += 2;
|
76 | return short;
|
77 | }
|
78 |
|
79 | |
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | readShort(offset) {
|
86 | return this._buffer.readInt16LE(offset);
|
87 | }
|
88 |
|
89 | |
90 |
|
91 |
|
92 |
|
93 |
|
94 | readNextDWord() {
|
95 | const dWord = this._buffer.readUInt32LE(this._offset);
|
96 | this._offset += 4;
|
97 | return dWord;
|
98 | }
|
99 |
|
100 | |
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | readDWord(offset) {
|
107 | return this._buffer.readUInt32LE(offset);
|
108 | }
|
109 |
|
110 | |
111 |
|
112 |
|
113 |
|
114 |
|
115 | readNextLong() {
|
116 | const long = this._buffer.readInt32LE(this._offset);
|
117 | this._offset += 4;
|
118 | return long;
|
119 | }
|
120 |
|
121 | |
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 | readLong(offset) {
|
128 | return this._buffer.readInt32LE(offset);
|
129 | }
|
130 |
|
131 | |
132 |
|
133 |
|
134 |
|
135 |
|
136 | readNextFixed() {
|
137 | const fixed = this._buffer.readFloatLE(this._offset);
|
138 | this._offset += 4;
|
139 | return fixed;
|
140 | }
|
141 |
|
142 | |
143 |
|
144 |
|
145 |
|
146 |
|
147 | readFixed(offset) {
|
148 | return this._buffer.readFloatLE(offset);
|
149 | }
|
150 |
|
151 | |
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | readNextBytes(numBytes) {
|
158 | let strBuff = Buffer.alloc(numBytes);
|
159 | for (let i = 0; i < numBytes; i++) {
|
160 | strBuff.writeUInt8(this.readNextByte(), i);
|
161 | }
|
162 | return strBuff.toString();
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 | readNextRawBytes(numBytes) {
|
172 | let buff = Buffer.alloc(numBytes);
|
173 | for (let i = 0; i < numBytes; i++) {
|
174 | buff.writeUInt8(this.readNextByte(), i);
|
175 | }
|
176 | return buff;
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 | readRawBytes(numBytes, b, offset) {
|
188 | let buff = Buffer.alloc(numBytes - offset);
|
189 | for (let i = 0; i < numBytes - offset; i++) {
|
190 | buff.writeUInt8(b.readUInt8(offset + i), i);
|
191 | }
|
192 | return buff;
|
193 | }
|
194 |
|
195 | |
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | readNextString() {
|
202 | const numBytes = this.readNextWord();
|
203 | return this.readNextBytes(numBytes);
|
204 | }
|
205 |
|
206 | |
207 |
|
208 |
|
209 |
|
210 |
|
211 | skipBytes(numBytes) {
|
212 | this._offset += numBytes;
|
213 | }
|
214 |
|
215 | |
216 |
|
217 |
|
218 |
|
219 |
|
220 | readHeader() {
|
221 | this.fileSize = this.readNextDWord();
|
222 |
|
223 |
|
224 | this.readNextWord();
|
225 | this.numFrames = this.readNextWord();
|
226 | this.width = this.readNextWord();
|
227 | this.height = this.readNextWord();
|
228 | this.colorDepth = this.readNextWord();
|
229 | |
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 | this.skipBytes(14);
|
237 | this.paletteIndex = this.readNextByte();
|
238 |
|
239 | this.skipBytes(3);
|
240 | this.numColors = this.readNextWord();
|
241 | const pixW = this.readNextByte();
|
242 | const pixH = this.readNextByte();
|
243 | this.pixelRatio = `${pixW}:${pixH}`;
|
244 | |
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | this.skipBytes(92);
|
254 | return this.numFrames;
|
255 | }
|
256 |
|
257 | |
258 |
|
259 |
|
260 | readFrame() {
|
261 | const bytesInFrame = this.readNextDWord();
|
262 |
|
263 |
|
264 |
|
265 | this.skipBytes(2);
|
266 |
|
267 | const oldChunk = this.readNextWord();
|
268 | const frameDuration = this.readNextWord();
|
269 |
|
270 | this.skipBytes(2);
|
271 | const newChunk = this.readNextDWord();
|
272 | let cels = [];
|
273 | for(let i = 0; i < newChunk; i ++) {
|
274 | let chunkData = this.readChunk();
|
275 | switch(chunkData.type) {
|
276 | case 0x0004:
|
277 | case 0x0011:
|
278 | case 0x2016:
|
279 | case 0x2017:
|
280 | case 0x2020:
|
281 | this.skipBytes(chunkData.chunkSize - 6);
|
282 | break;
|
283 | case 0x2022:
|
284 | this.readSliceChunk();
|
285 | break;
|
286 | case 0x2004:
|
287 | this.readLayerChunk();
|
288 | break;
|
289 | case 0x2005:
|
290 | let celData = this.readCelChunk(chunkData.chunkSize);
|
291 | cels.push(celData);
|
292 | break;
|
293 | case 0x2007:
|
294 | this.readColorProfileChunk();
|
295 | break;
|
296 | case 0x2018:
|
297 | this.readFrameTagsChunk();
|
298 | break;
|
299 | case 0x2019:
|
300 | this.palette = this.readPaletteChunk();
|
301 | break;
|
302 | case 0x2023:
|
303 | this.tilesets.push(this.readTilesetChunk());
|
304 | break;
|
305 | default:
|
306 | this.skipBytes(chunkData.chunkSize - 6);
|
307 | }
|
308 | }
|
309 | this.frames.push({ bytesInFrame,
|
310 | frameDuration,
|
311 | numChunks: newChunk,
|
312 | cels});
|
313 | }
|
314 |
|
315 | |
316 |
|
317 |
|
318 |
|
319 | readColorProfileChunk() {
|
320 | const types = [
|
321 | 'None',
|
322 | 'sRGB',
|
323 | 'ICC'
|
324 | ]
|
325 | const typeInd = this.readNextWord();
|
326 | const type = types[typeInd];
|
327 | const flag = this.readNextWord();
|
328 | const fGamma = this.readNextFixed();
|
329 | this.skipBytes(8);
|
330 | if (typeInd === 2) {
|
331 |
|
332 | const skip = this.readNextDWord();
|
333 | this.skipBytes(skip);
|
334 | }
|
335 | this.colorProfile = {
|
336 | type,
|
337 | flag,
|
338 | fGamma};
|
339 | }
|
340 |
|
341 | |
342 |
|
343 |
|
344 |
|
345 | readFrameTagsChunk() {
|
346 | const loops = [
|
347 | 'Forward',
|
348 | 'Reverse',
|
349 | 'Ping-pong',
|
350 | 'Ping-pong Reverse'
|
351 | ]
|
352 | const numTags = this.readNextWord();
|
353 | this.skipBytes(8);
|
354 | for(let i = 0; i < numTags; i ++) {
|
355 | let tag = {};
|
356 | tag.from = this.readNextWord();
|
357 | tag.to = this.readNextWord();
|
358 | const loopsInd = this.readNextByte();
|
359 | tag.animDirection = loops[loopsInd];
|
360 | tag.repeat = this.readNextWord();
|
361 | this.skipBytes(6);
|
362 | tag.color = this.readNextRawBytes(3).toString('hex');
|
363 | this.skipBytes(1);
|
364 | tag.name = this.readNextString();
|
365 | this.tags.push(tag);
|
366 | }
|
367 | }
|
368 |
|
369 | |
370 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 | readPaletteChunk() {
|
376 | const paletteSize = this.readNextDWord();
|
377 | const firstColor = this.readNextDWord();
|
378 | const secondColor = this.readNextDWord();
|
379 | this.skipBytes(8);
|
380 | let colors = [];
|
381 | for (let i = 0; i < paletteSize; i++) {
|
382 | let flag = this.readNextWord();
|
383 | let red = this.readNextByte();
|
384 | let green = this.readNextByte();
|
385 | let blue = this.readNextByte();
|
386 | let alpha = this.readNextByte();
|
387 | let name;
|
388 | if (flag === 1) {
|
389 | name = this.readNextString();
|
390 | }
|
391 | colors.push({
|
392 | red,
|
393 | green,
|
394 | blue,
|
395 | alpha,
|
396 | name: name !== undefined ? name : "none"
|
397 | });
|
398 | }
|
399 | let palette = {
|
400 | paletteSize,
|
401 | firstColor,
|
402 | lastColor: secondColor,
|
403 | colors
|
404 | }
|
405 | this.colorDepth === 8 ? palette.index = this.paletteIndex : '';
|
406 | return palette;
|
407 | }
|
408 |
|
409 | |
410 |
|
411 |
|
412 |
|
413 | readTilesetChunk() {
|
414 | const id = this.readNextDWord();
|
415 | const flags = this.readNextDWord();
|
416 | const tileCount = this.readNextDWord();
|
417 | const tileWidth = this.readNextWord();
|
418 | const tileHeight = this.readNextWord();
|
419 | this.skipBytes(16);
|
420 | const name = this.readNextString();
|
421 | const tileset = {
|
422 | id,
|
423 | tileCount,
|
424 | tileWidth,
|
425 | tileHeight,
|
426 | name };
|
427 | if ((flags & 1) !== 0) {
|
428 | tileset.externalFile = {}
|
429 | tileset.externalFile.id = this.readNextDWord();
|
430 | tileset.externalFile.tilesetId = this.readNextDWord();
|
431 | }
|
432 | if ((flags & 2) !== 0) {
|
433 | const dataLength = this.readNextDWord();
|
434 | const buff = this.readNextRawBytes(dataLength);
|
435 | tileset.rawTilesetData = zlib.inflateSync(buff);
|
436 | }
|
437 | return tileset;
|
438 | }
|
439 |
|
440 | |
441 |
|
442 |
|
443 |
|
444 | readSliceChunk() {
|
445 | const numSliceKeys = this.readNextDWord();
|
446 | const flags = this.readNextDWord();
|
447 | this.skipBytes(4);
|
448 | const name = this.readNextString();
|
449 | const keys = [];
|
450 | for(let i = 0; i < numSliceKeys; i ++) {
|
451 | const frameNumber = this.readNextDWord();
|
452 | const x = this.readNextLong();
|
453 | const y = this.readNextLong();
|
454 | const width = this.readNextDWord();
|
455 | const height = this.readNextDWord();
|
456 | const key = { frameNumber, x, y, width, height };
|
457 | if((flags & 1) !== 0) {
|
458 | key.patch = this.readSlicePatchChunk();
|
459 | }
|
460 | if((flags & 2) !== 0) {
|
461 | key.pivot = this.readSlicePivotChunk();
|
462 | }
|
463 | keys.push(key);
|
464 | }
|
465 | this.slices.push({ flags, name, keys });
|
466 | }
|
467 |
|
468 | |
469 |
|
470 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 | readSlicePatchChunk() {
|
478 | const x = this.readNextLong();
|
479 | const y = this.readNextLong();
|
480 | const width = this.readNextDWord();
|
481 | const height = this.readNextDWord();
|
482 | return { x, y, width, height };
|
483 | }
|
484 |
|
485 | |
486 |
|
487 |
|
488 |
|
489 |
|
490 |
|
491 |
|
492 | readSlicePivotChunk() {
|
493 | const x = this.readNextLong();
|
494 | const y = this.readNextLong();
|
495 | return { x, y };
|
496 | }
|
497 |
|
498 | |
499 |
|
500 |
|
501 |
|
502 | readLayerChunk() {
|
503 | const layer = {}
|
504 | layer.flags = this.readNextWord();
|
505 | layer.type = this.readNextWord();
|
506 | layer.layerChildLevel = this.readNextWord();
|
507 | this.skipBytes(4);
|
508 | layer.blendMode = this.readNextWord();
|
509 | layer.opacity = this.readNextByte();
|
510 | this.skipBytes(3);
|
511 | layer.name = this.readNextString();
|
512 | if (layer.type == 2) {
|
513 | layer.tilesetIndex =this.readNextDWord()
|
514 | }
|
515 | this.layers.push(layer);
|
516 | }
|
517 |
|
518 | |
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 | readCelChunk(chunkSize) {
|
526 | const layerIndex = this.readNextWord();
|
527 | const x = this.readNextShort();
|
528 | const y = this.readNextShort();
|
529 | const opacity = this.readNextByte();
|
530 | const celType = this.readNextWord();
|
531 | const zIndex = this.readNextShort();
|
532 | this.skipBytes(5);
|
533 | if (celType === 1) {
|
534 | return {
|
535 | layerIndex,
|
536 | xpos: x,
|
537 | ypos: y,
|
538 | opacity,
|
539 | celType,
|
540 | zIndex,
|
541 | w: 0,
|
542 | h: 0,
|
543 | rawCelData: undefined,
|
544 | link: this.readNextWord()
|
545 | };
|
546 | }
|
547 | const w = this.readNextWord();
|
548 | const h = this.readNextWord();
|
549 | const chunkBase = {
|
550 | layerIndex,
|
551 | xpos: x,
|
552 | ypos: y,
|
553 | opacity,
|
554 | celType,
|
555 | zIndex,
|
556 | w,
|
557 | h
|
558 | };
|
559 | if (celType === 0 || celType === 2) {
|
560 | const buff = this.readNextRawBytes(chunkSize - 26);
|
561 | return {
|
562 | ...chunkBase,
|
563 | rawCelData: celType === 2 ? zlib.inflateSync(buff) : buff
|
564 | }
|
565 | }
|
566 | if (celType === 3) {
|
567 | return { ...chunkBase, ...this.readTilemapCelChunk(chunkSize) }
|
568 | }
|
569 | }
|
570 | readTilemapCelChunk(chunkSize) {
|
571 | const bitsPerTile = this.readNextWord();
|
572 | const bitmaskForTileId = this.readNextDWord();
|
573 | const bitmaskForXFlip = this.readNextDWord();
|
574 | const bitmaskForYFlip = this.readNextDWord();
|
575 | const bitmaskFor90CWRotation = this.readNextDWord();
|
576 | this.skipBytes(10);
|
577 | const buff = this.readNextRawBytes(chunkSize - 54);
|
578 | const rawCelData = zlib.inflateSync(buff);
|
579 | const tilemapMetadata = {
|
580 | bitsPerTile,
|
581 | bitmaskForTileId,
|
582 | bitmaskForXFlip,
|
583 | bitmaskForYFlip,
|
584 | bitmaskFor90CWRotation };
|
585 | return { tilemapMetadata, rawCelData };
|
586 | }
|
587 |
|
588 | |
589 |
|
590 |
|
591 |
|
592 |
|
593 |
|
594 |
|
595 | readChunk() {
|
596 | const cSize = this.readNextDWord();
|
597 | const type = this.readNextWord();
|
598 | return {chunkSize: cSize, type: type};
|
599 | }
|
600 |
|
601 | |
602 |
|
603 |
|
604 | parse() {
|
605 | const numFrames = this.readHeader();
|
606 | for(let i = 0; i < numFrames; i ++) {
|
607 | this.readFrame();
|
608 | }
|
609 | for(let i = 0; i < numFrames; i ++) {
|
610 | for (let j = 0; j < this.frames[i].cels.length; j++) {
|
611 | const cel = this.frames[i].cels[j];
|
612 | if (cel.celType === 1) {
|
613 | for (let k = 0; k < this.frames[cel.link].cels.length; k++) {
|
614 | const srcCel = this.frames[cel.link].cels[k];
|
615 | if (srcCel.layerIndex === cel.layerIndex) {
|
616 | cel.w = srcCel.w;
|
617 | cel.h = srcCel.h;
|
618 | cel.rawCelData = srcCel.rawCelData;
|
619 | }
|
620 | if (cel.rawCelData) {
|
621 | break;
|
622 | }
|
623 | }
|
624 | }
|
625 | }
|
626 | }
|
627 | }
|
628 |
|
629 | |
630 |
|
631 |
|
632 |
|
633 |
|
634 |
|
635 |
|
636 | formatBytes(bytes,decimals) {
|
637 | if (bytes === 0) {
|
638 | return '0 Byte';
|
639 | }
|
640 | const k = 1024;
|
641 | const dm = decimals + 1 || 3;
|
642 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
643 | const i = Math.floor(Math.log(bytes) / Math.log(k));
|
644 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
645 | };
|
646 |
|
647 | |
648 |
|
649 |
|
650 |
|
651 |
|
652 | toJSON() {
|
653 | return {
|
654 | fileSize: this.fileSize,
|
655 | numFrames: this.numFrames,
|
656 | frames: this.frames.map(frame => {
|
657 | return {
|
658 | size: frame.bytesInFrame,
|
659 | duration: frame.frameDuration,
|
660 | chunks: frame.numChunks,
|
661 | cels: frame.cels.map(cel => {
|
662 | return {
|
663 | layerIndex: cel.layerIndex,
|
664 | xpos: cel.xpos,
|
665 | ypos: cel.ypos,
|
666 | opacity: cel.opacity,
|
667 | celType: cel.celType,
|
668 | w: cel.w,
|
669 | h: cel.h,
|
670 | rawCelData: 'buffer'
|
671 | }
|
672 | }) }
|
673 | }),
|
674 | palette: this.palette,
|
675 | tilesets: this.tilesets,
|
676 | width: this.width,
|
677 | height: this.height,
|
678 | colorDepth: this.colorDepth,
|
679 | numColors: this.numColors,
|
680 | pixelRatio: this.pixelRatio,
|
681 | layers: this.layers,
|
682 | slices: this.slices
|
683 | };
|
684 | }
|
685 | }
|
686 |
|
687 | module.exports = Aseprite;
|