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