-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathindex.js
More file actions
99 lines (86 loc) · 3.64 KB
/
index.js
File metadata and controls
99 lines (86 loc) · 3.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import crc from 'crc';
// https://stackoverflow.com/a/39244362/2715716
// TODO: Refactor it so that it allocates one buffer at the start
// which is sized approximately so that it fits the signature, IHDR from the 1st
// frame, ACTL, the entire total of the buffers' sizes and IEND, then gets
// written to: the signature, the IHDR from the initial frame, the FCTL and FDAT
// of all the buffers in a for-of loop and then an IEND after the loop and after
// this, slice the buffer from zero to the actual length it got written to to
// get rid of the excess (which wouldn't break the PNG, there can be anything
// after IEND, but would make it larger than necessary, albeit not by very much
// at all)
export default function makeApng(/** @type {Buffer[]} */ buffers, /** @type {number} */ delay) {
function findChunk(buffer, type, offset = 8) {
while (offset < buffer.length) {
const chunkLength = buffer.readUInt32BE(offset);
const chunkType = buffer.slice(offset + 4, offset + 8).toString('ascii');
if (chunkType === type) {
return buffer.slice(offset, offset + chunkLength + 12);
}
offset += 4 + 4 + chunkLength + 4;
}
return null;
}
const actl = Buffer.alloc(20);
actl.writeUInt32BE(8, 0); // Length of chunk
actl.write('acTL', 4); // Type of chunk
actl.writeUInt32BE(buffers.length, 8); // Number of frames
actl.writeUInt32BE(0, 12); // Number of times to loop (0 - infinite)
actl.writeUInt32BE(crc.crc32(actl.slice(4, 16)), 16); // CRC
let sequenceNumber = 0;
const frames = buffers.map((data, index) => {
const ihdr = findChunk(data, 'IHDR');
if (ihdr === null) {
throw new Error('IHDR chunk not found!');
}
const fctl = Buffer.alloc(38);
fctl.writeUInt32BE(26, 0); // Length of chunk
fctl.write('fcTL', 4); // Type of chunk
fctl.writeUInt32BE(sequenceNumber++, 8); // Sequence number
fctl.writeUInt32BE(ihdr.readUInt32BE(8), 12); // Width
fctl.writeUInt32BE(ihdr.readUInt32BE(12), 16); // Height
fctl.writeUInt32BE(0, 20); // X offset
fctl.writeUInt32BE(0, 24); // Y offset
const { numerator, denominator } = delay(index);
fctl.writeUInt16BE(numerator, 28); // Frame delay - fraction numerator
fctl.writeUInt16BE(denominator, 30); // Frame delay - fraction denominator
fctl.writeUInt8(0, 32); // Dispose mode
fctl.writeUInt8(0, 33); // Blend mode
fctl.writeUInt32BE(crc.crc32(fctl.slice(4, 34)), 34); // CRC
let offset = 8;
const fdats = [];
while (true) {
const idat = findChunk(data, 'IDAT', offset);
if (idat === null) {
if (offset === 8) {
throw new Error('No IDAT chunks found!');
}
else {
break;
}
}
offset = idat.byteOffset + idat.length;
// All IDAT chunks except first one are converted to fdAT chunks
if (index === 0) {
fdats.push(idat);
} else {
const length = idat.length + 4;
const fdat = Buffer.alloc(length);
fdat.writeUInt32BE(length - 12, 0); // Length of chunk
fdat.write('fdAT', 4); // Type of chunk
fdat.writeUInt32BE(sequenceNumber++, 8); // Sequence number
idat.copy(fdat, 12, 8); // Image data
fdat.writeUInt32BE(crc.crc32(fdat.slice(4, length - 4)), length - 4); // CRC
fdats.push(fdat);
}
}
return Buffer.concat([fctl, ...fdats]);
});
const signature = Buffer.from('89504e470d0a1a0a', 'hex');
const ihdr = findChunk(buffers[0], 'IHDR');
if (ihdr === null) {
throw new Error('IHDR chunk not found!');
}
const iend = Buffer.from('0000000049454e44ae426082', 'hex');
return Buffer.concat([signature, ihdr, actl, ...frames, iend]);
}