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