UNPKG

18.4 kBJavaScriptView Raw
1const zlib = require('zlib');
2
3/**
4 * Aseprite Class to consume an Aseprite file and get information from it
5 */
6class 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 * Reads the next byte (8-bit unsigned) value in the buffer
28 *
29 * @returns {number}
30 */
31 readNextByte() {
32 const nextByte = this._buffer.readUInt8(this._offset);
33 this._offset += 1;
34 return nextByte;
35 }
36
37 /**
38 * Reads a byte (8-bit unsigned) value in the buffer at a specific location
39 *
40 * @param {number} offset - Offset location in the buffer
41 * @returns {number}
42 */
43 readByte(offset) {
44 return this._buffer.readUInt8(offset);
45 }
46
47 /**
48 * Reads the next word (16-bit unsigned) value in the buffer
49 *
50 * @returns {number}
51 */
52 readNextWord() {
53 const word = this._buffer.readUInt16LE(this._offset);
54 this._offset += 2;
55 return word;
56 }
57
58 /**
59 * Reads a word (16-bit unsigned) value at a specific location
60 *
61 * @param {number} offset - Offset location in the buffer
62 * @returns {number}
63 */
64 readWord(offset) {
65 return this._buffer.readUInt16LE(offset);
66 }
67
68 /**
69 * Reads the next short (16-bit signed) value in the buffer
70 *
71 * @returns {number}
72 */
73 readNextShort() {
74 const short = this._buffer.readInt16LE(this._offset);
75 this._offset += 2;
76 return short;
77 }
78
79 /**
80 * Reads a short (16-bit signed) value at a specific location
81 *
82 * @param {number} offset - Offset location in the buffer
83 * @returns {number}
84 */
85 readShort(offset) {
86 return this._buffer.readInt16LE(offset);
87 }
88
89 /**
90 * Reads the next DWord (32-bit unsigned) value from the buffer
91 *
92 * @returns {number}
93 */
94 readNextDWord() {
95 const dWord = this._buffer.readUInt32LE(this._offset);
96 this._offset += 4;
97 return dWord;
98 }
99
100 /**
101 * Reads a DWord (32-bit unsigned) value at a specific location
102 *
103 * @param {number} offset - Offset location in the buffer
104 * @returns {number}
105 */
106 readDWord(offset) {
107 return this._buffer.readUInt32LE(offset);
108 }
109
110 /**
111 * Reads the next long (32-bit signed) value from the buffer
112 *
113 * @returns {number}
114 */
115 readNextLong() {
116 const long = this._buffer.readInt32LE(this._offset);
117 this._offset += 4;
118 return long;
119 }
120
121 /**
122 * Reads a long (32-bit signed) value at a specific location
123 *
124 * @param {number} offset - Offset location in the buffer
125 * @returns {number}
126 */
127 readLong(offset) {
128 return this._buffer.readInt32LE(offset);
129 }
130
131 /**
132 * Reads the next fixed (32-bit fixed point 16.16) value from the buffer
133 *
134 * @returns {number}
135 */
136 readNextFixed() {
137 const fixed = this._buffer.readFloatLE(this._offset);
138 this._offset += 4;
139 return fixed;
140 }
141
142 /**
143 * Reads a fixed (32-bit fixed point 16.16) value at a specific location
144 * @param {number} offset - Offset location in the buffer
145 * @returns {number}
146 */
147 readFixed(offset) {
148 return this._buffer.readFloatLE(offset);
149 }
150
151 /**
152 * Reads the next numBytes bytes and creates a string from the buffer
153 *
154 * @param {number} numBytes - Number of bytes to read
155 * @returns {string}
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 * Copy the next numBytes bytes of the buffer into a new buffer
167 *
168 * @param {number} numBytes - Number of bytes to read
169 * @returns {Buffer}
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 * Create a new buffer with numBytes size, offset by a value, from a buffer
181 *
182 * @param {number} numBytes - Number of bytes to read
183 * @param {Buffer} b - Buffer to read from
184 * @param {number} offset - Offset value to start reading from
185 * @returns {Buffer}
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 * Reads the next word to get the length of the string, then reads the string
197 * and returns it
198 *
199 * @returns {string}
200 */
201 readNextString() {
202 const numBytes = this.readNextWord();
203 return this.readNextBytes(numBytes);
204 }
205
206 /**
207 * Skips a number of bytes in the buffer
208 *
209 * @param {number} numBytes - Number of bytes to skip
210 */
211 skipBytes(numBytes) {
212 this._offset += numBytes;
213 }
214
215 /**
216 * Reads the 128-byte header of an Aseprite file and stores the information
217 *
218 * @returns {number} Number of frames in the file
219 */
220 readHeader() {
221 this.fileSize = this.readNextDWord();
222 // Consume the next word (16-bit unsigned) value in the buffer
223 // to skip the "Magic number" (0xA5E0)
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 * Skip 14 bytes to account for:
231 * Dword - Layer opacity flag
232 * Word - deprecated speed (ms) between frame
233 * Dword - 0 value
234 * Dword - 0 value
235 */
236 this.skipBytes(14);
237 this.paletteIndex = this.readNextByte();
238 // Skip 3 bytes for empty data
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 * Skip 92 bytes to account for:
246 * Short - X position of the grid
247 * Short - Y position of the grid
248 * Word - Grid width
249 * Word - Grid height, defaults to 0 if there is no grid
250 * (Defaults to 16x16 if there is no grid size)
251 * Last 84 bytes is set to 0 for future use
252 */
253 this.skipBytes(92);
254 return this.numFrames;
255 }
256
257 /**
258 * Reads a frame and stores the information
259 */
260 readFrame() {
261 const bytesInFrame = this.readNextDWord();
262 // skip bytes for the magic number (0xF1FA)
263 // TODO: Add a check in to make sure the magic number is correct
264 // (this should help to make sure we're doing what we're supposed to)
265 this.skipBytes(2);
266 // TODO Use the old chunk of data if `newChunk` is 0
267 const oldChunk = this.readNextWord();
268 const frameDuration = this.readNextWord();
269 // Skip 2 bytes that are reserved for future use
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: // ignore unknown chunk types
306 this.skipBytes(chunkData.chunkSize - 6);
307 }
308 }
309 this.frames.push({ bytesInFrame,
310 frameDuration,
311 numChunks: newChunk,
312 cels});
313 }
314
315 /**
316 * Reads the Color Profile Chunk and stores the information
317 * Color Profile Chunk is type 0x2007
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 //TODO: Handle ICC profile data properly instead of skipping
332 const skip = this.readNextDWord();
333 this.skipBytes(skip);
334 }
335 this.colorProfile = {
336 type,
337 flag,
338 fGamma};
339 }
340
341 /**
342 * Reads the Tags Chunk and stores the information
343 * Tags Cunk is type 0x2018
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 * Reads the Palette Chunk and stores the information
371 * Palette Chunk is type 0x2019
372 *
373 * @returns {Palette}
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 * Reads the Tileset Chunk and stores the information
411 * Tileset Chunk is type 0x2023
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 * Reads the Slice Chunk and stores the information
442 * Slice Chunk is type 0x2022
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 * Reads the Patch portion of a Slice Chunk
470 *
471 * @returns {Object} patch - Patch information that was in the chunk
472 * @returns {number} patch.x - Patch X location
473 * @returns {number} patch.y - Patch Y location
474 * @returns {number} patch.width - Patch width
475 * @returns {number} patch.height - Patch height
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 * Reads the Pivot portion of a Slice Chunk
487 *
488 * @returns {Object} pivot - Pivot information that was in the chunk
489 * @returns {number} pivot.x - Pivot X location
490 * @returns {number} pivot.y - Pivot Y location
491 */
492 readSlicePivotChunk() {
493 const x = this.readNextLong();
494 const y = this.readNextLong();
495 return { x, y };
496 }
497
498 /**
499 * Reads the Layer Chunk and stores the information
500 * Layer Chunk is type 0x2004
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 * Reads a Cel Chunk in its entirety and returns the information
520 * Cel Chunk is type 0x2005
521 *
522 * @param {number} chunkSize - Size of the Cel Chunk to read
523 * @returns {Object} Cel information
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); // take the first 20 bytes off for the data above and chunk info
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 * Reads the next Chunk Info block to get how large and what type the next Chunk is
590 *
591 * @returns {Object} chunkInfo
592 * @returns {number} chunkInfo.chunkSize - The size of the Chunk read
593 * @returns {number} chunkInfo.type - The type of the Chunk
594 */
595 readChunk() {
596 const cSize = this.readNextDWord();
597 const type = this.readNextWord();
598 return {chunkSize: cSize, type: type};
599 }
600
601 /**
602 * Processes the Aseprite file and stores the information
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 * Converts an amount of Bytes to a human readable format
631 *
632 * @param {number} bytes - Bytes to format
633 * @param {number} decimals - Number of decimals to format the number to
634 * @returns {string} - Amount of Bytes formatted in a more human readable format
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 * Attempts to return the data in a string format
649 *
650 * @returns {string}
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
687module.exports = Aseprite;