@engine-room/after-effects-mcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/bin/server.js +376 -86
- package/package.json +1 -1
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/framecache.js +98 -0
- package/panel/client/main.js +261 -20
- package/panel/client/mogrt.js +328 -0
- package/panel/client/pngcodec.js +402 -0
- package/panel/jsx/bundle.jsx +1187 -157
- package/panel/package.json +1 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// pngcodec.js — the panel's PNG normaliser.
|
|
2
|
+
//
|
|
3
|
+
// After Effects writes screenshot frames at the project's colour depth, so a
|
|
4
|
+
// 16-bit project produces 16-bit-per-channel PNGs. Plenty of decoders — the ones
|
|
5
|
+
// on the far side of an MCP image content block included — reject those outright
|
|
6
|
+
// with "Could not process image", so a perfectly good render arrives useless.
|
|
7
|
+
// This converts to 8 bits per channel before the panel base64-encodes, and
|
|
8
|
+
// passes anything already 8-bit through byte-for-byte untouched.
|
|
9
|
+
//
|
|
10
|
+
// It also does the two other things that need the pixels rather than the file:
|
|
11
|
+
//
|
|
12
|
+
// * reports a frame whose every pixel is fully transparent, because "this
|
|
13
|
+
// frame is empty" is the useful reading of a ~5KB PNG that decoders choke
|
|
14
|
+
// on, and it usually means the caller is looking at the wrong time or a
|
|
15
|
+
// disabled layer;
|
|
16
|
+
// * hands back the decoded samples so the caller can hash them. Two different
|
|
17
|
+
// screenshot requests that produce byte-identical pixels are how a stale
|
|
18
|
+
// render buffer shows itself (see framecache.js).
|
|
19
|
+
//
|
|
20
|
+
// Cost, since it is paid on every screenshot and not only on 16-bit ones: an
|
|
21
|
+
// already-8-bit frame is still inflated and un-filtered, because that is the
|
|
22
|
+
// only way to know whether it is empty and the only hash that cannot be thrown
|
|
23
|
+
// off by an encoder writing the same picture two ways. That is one pass over
|
|
24
|
+
// width*height*channels bytes — negligible at the downsample the guides ask
|
|
25
|
+
// for, and small next to a render measured in seconds even at full 4K. If it
|
|
26
|
+
// ever stops being negligible, note that an empty frame compresses to a few KB,
|
|
27
|
+
// so a size threshold could skip the decode and fall back to the IDAT hash.
|
|
28
|
+
//
|
|
29
|
+
// A CommonJS module on purpose: `client/main.js` requires it inside CEP's mixed
|
|
30
|
+
// context, and `tests/unit/png-codec.mjs` requires the same file under plain
|
|
31
|
+
// Node. There is no After Effects on a CI runner and this is real image code, so
|
|
32
|
+
// it has to be exercisable without one. Node builtins only — adding a runtime
|
|
33
|
+
// dependency here would mean shipping another directory into the CEP extension,
|
|
34
|
+
// which is exactly the problem `ws` already causes.
|
|
35
|
+
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
var zlib = require("zlib");
|
|
39
|
+
|
|
40
|
+
var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
41
|
+
|
|
42
|
+
// Deflate level: the output is a diagnostic image that has to fit in an agent's
|
|
43
|
+
// context, so size matters — but a 4K frame is 33MB of samples and level 9 costs
|
|
44
|
+
// seconds on top of a render that already took seconds. 6 is within a few
|
|
45
|
+
// percent of 9 here and several times faster.
|
|
46
|
+
var DEFLATE_LEVEL = 6;
|
|
47
|
+
|
|
48
|
+
function channelsFor(colorType) {
|
|
49
|
+
if (colorType === 0) return 1; // greyscale
|
|
50
|
+
if (colorType === 2) return 3; // truecolour
|
|
51
|
+
if (colorType === 3) return 1; // indexed
|
|
52
|
+
if (colorType === 4) return 2; // greyscale + alpha
|
|
53
|
+
if (colorType === 6) return 4; // truecolour + alpha
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function alphaIndexFor(colorType) {
|
|
58
|
+
if (colorType === 4) return 1;
|
|
59
|
+
if (colorType === 6) return 3;
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------- CRC-32, as specified in the PNG spec ----------
|
|
64
|
+
var CRC_TABLE = (function () {
|
|
65
|
+
var table = new Int32Array(256);
|
|
66
|
+
for (var n = 0; n < 256; n++) {
|
|
67
|
+
var c = n;
|
|
68
|
+
for (var k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
69
|
+
table[n] = c;
|
|
70
|
+
}
|
|
71
|
+
return table;
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
function crc32(bytes) {
|
|
75
|
+
var c = 0xffffffff;
|
|
76
|
+
for (var i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
77
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- Reading ----------
|
|
81
|
+
function readChunks(buf) {
|
|
82
|
+
if (buf.length < 8) throw new Error("not a PNG: only " + buf.length + " bytes");
|
|
83
|
+
for (var i = 0; i < 8; i++) {
|
|
84
|
+
if (buf[i] !== SIGNATURE[i]) throw new Error("not a PNG: signature mismatch at byte " + i);
|
|
85
|
+
}
|
|
86
|
+
var chunks = [];
|
|
87
|
+
var pos = 8;
|
|
88
|
+
while (pos + 8 <= buf.length) {
|
|
89
|
+
var len = buf.readUInt32BE(pos);
|
|
90
|
+
var type = buf.toString("ascii", pos + 4, pos + 8);
|
|
91
|
+
var dataStart = pos + 8;
|
|
92
|
+
var dataEnd = dataStart + len;
|
|
93
|
+
if (dataEnd + 4 > buf.length) {
|
|
94
|
+
throw new Error("truncated PNG: chunk " + type + " runs past the end of the file");
|
|
95
|
+
}
|
|
96
|
+
chunks.push({ type: type, data: buf.subarray(dataStart, dataEnd) });
|
|
97
|
+
pos = dataEnd + 4;
|
|
98
|
+
if (type === "IEND") return chunks;
|
|
99
|
+
}
|
|
100
|
+
throw new Error("truncated PNG: no IEND chunk");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseHeader(chunks) {
|
|
104
|
+
if (!chunks.length || chunks[0].type !== "IHDR") {
|
|
105
|
+
throw new Error("malformed PNG: IHDR is not the first chunk");
|
|
106
|
+
}
|
|
107
|
+
var d = chunks[0].data;
|
|
108
|
+
if (d.length !== 13) throw new Error("malformed PNG: IHDR is " + d.length + " bytes, expected 13");
|
|
109
|
+
var h = {
|
|
110
|
+
width: d.readUInt32BE(0),
|
|
111
|
+
height: d.readUInt32BE(4),
|
|
112
|
+
bitDepth: d[8],
|
|
113
|
+
colorType: d[9],
|
|
114
|
+
compression: d[10],
|
|
115
|
+
filter: d[11],
|
|
116
|
+
interlace: d[12],
|
|
117
|
+
};
|
|
118
|
+
if (h.width === 0 || h.height === 0) {
|
|
119
|
+
throw new Error("malformed PNG: " + h.width + "x" + h.height);
|
|
120
|
+
}
|
|
121
|
+
return h;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------- Filtering ----------
|
|
125
|
+
function paeth(a, b, c) {
|
|
126
|
+
var p = a + b - c;
|
|
127
|
+
var pa = p > a ? p - a : a - p;
|
|
128
|
+
var pb = p > b ? p - b : b - p;
|
|
129
|
+
var pc = p > c ? p - c : c - p;
|
|
130
|
+
if (pa <= pb && pa <= pc) return a;
|
|
131
|
+
if (pb <= pc) return b;
|
|
132
|
+
return c;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Reverse the per-scanline filters into a flat sample buffer. `raw` is the
|
|
136
|
+
// inflated IDAT stream: one filter-type byte followed by rowBytes of data, per
|
|
137
|
+
// row. Every filter reads back from bytes already reconstructed, so this has to
|
|
138
|
+
// run in order and read out of `out` rather than out of `raw`.
|
|
139
|
+
function unfilter(raw, width, height, bitDepth, channels) {
|
|
140
|
+
var bpp = Math.ceil((bitDepth * channels) / 8);
|
|
141
|
+
var rowBytes = Math.ceil((bitDepth * channels * width) / 8);
|
|
142
|
+
var expected = (rowBytes + 1) * height;
|
|
143
|
+
if (raw.length < expected) {
|
|
144
|
+
throw new Error("PNG pixel data is " + raw.length + " bytes, expected " + expected);
|
|
145
|
+
}
|
|
146
|
+
var out = Buffer.alloc(rowBytes * height);
|
|
147
|
+
var pos = 0;
|
|
148
|
+
for (var y = 0; y < height; y++) {
|
|
149
|
+
var filter = raw[pos];
|
|
150
|
+
pos += 1;
|
|
151
|
+
var o = y * rowBytes;
|
|
152
|
+
var up = o - rowBytes;
|
|
153
|
+
for (var x = 0; x < rowBytes; x++) {
|
|
154
|
+
var cur = raw[pos + x];
|
|
155
|
+
var a = x >= bpp ? out[o + x - bpp] : 0;
|
|
156
|
+
var b = y > 0 ? out[up + x] : 0;
|
|
157
|
+
var c = (x >= bpp && y > 0) ? out[up + x - bpp] : 0;
|
|
158
|
+
var v;
|
|
159
|
+
if (filter === 0) v = cur;
|
|
160
|
+
else if (filter === 1) v = cur + a;
|
|
161
|
+
else if (filter === 2) v = cur + b;
|
|
162
|
+
else if (filter === 3) v = cur + ((a + b) >> 1);
|
|
163
|
+
else if (filter === 4) v = cur + paeth(a, b, c);
|
|
164
|
+
else throw new Error("unknown PNG row filter " + filter + " on row " + y);
|
|
165
|
+
out[o + x] = v & 0xff;
|
|
166
|
+
}
|
|
167
|
+
pos += rowBytes;
|
|
168
|
+
}
|
|
169
|
+
return { data: out, rowBytes: rowBytes, bpp: bpp };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Re-apply a filter on the way out. Paeth on every row rather than picking the
|
|
173
|
+
// cheapest of the five per row: one pass instead of five, and on rendered frames
|
|
174
|
+
// it is within a few percent of the best choice.
|
|
175
|
+
function filterRowsPaeth(data, height, rowBytes, bpp) {
|
|
176
|
+
var out = Buffer.alloc((rowBytes + 1) * height);
|
|
177
|
+
var pos = 0;
|
|
178
|
+
for (var y = 0; y < height; y++) {
|
|
179
|
+
out[pos] = 4;
|
|
180
|
+
pos += 1;
|
|
181
|
+
var o = y * rowBytes;
|
|
182
|
+
var up = o - rowBytes;
|
|
183
|
+
for (var x = 0; x < rowBytes; x++) {
|
|
184
|
+
var a = x >= bpp ? data[o + x - bpp] : 0;
|
|
185
|
+
var b = y > 0 ? data[up + x] : 0;
|
|
186
|
+
var c = (x >= bpp && y > 0) ? data[up + x - bpp] : 0;
|
|
187
|
+
out[pos + x] = (data[o + x] - paeth(a, b, c)) & 0xff;
|
|
188
|
+
}
|
|
189
|
+
pos += rowBytes;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------- Writing ----------
|
|
195
|
+
function chunk(type, data) {
|
|
196
|
+
var out = Buffer.alloc(data.length + 12);
|
|
197
|
+
out.writeUInt32BE(data.length, 0);
|
|
198
|
+
out.write(type, 4, 4, "ascii");
|
|
199
|
+
data.copy(out, 8);
|
|
200
|
+
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Emit an 8-bit PNG from flat samples. Ancillary chunks from the source are
|
|
206
|
+
* deliberately not carried over: the ones that would matter (tRNS, sBIT, bKGD)
|
|
207
|
+
* hold bit-depth-dependent values that would be wrong at 8 bits, and the rest
|
|
208
|
+
* describe colour intent that no decoder of a diagnostic screenshot acts on.
|
|
209
|
+
*/
|
|
210
|
+
function encodePng8(width, height, colorType, data8) {
|
|
211
|
+
var channels = channelsFor(colorType);
|
|
212
|
+
if (channels === 0) throw new Error("cannot encode PNG colour type " + colorType);
|
|
213
|
+
var rowBytes = width * channels;
|
|
214
|
+
if (data8.length < rowBytes * height) {
|
|
215
|
+
throw new Error("sample buffer is " + data8.length + " bytes, expected " + rowBytes * height);
|
|
216
|
+
}
|
|
217
|
+
var ihdr = Buffer.alloc(13);
|
|
218
|
+
ihdr.writeUInt32BE(width, 0);
|
|
219
|
+
ihdr.writeUInt32BE(height, 4);
|
|
220
|
+
ihdr[8] = 8; // bit depth
|
|
221
|
+
ihdr[9] = colorType;
|
|
222
|
+
ihdr[10] = 0; // deflate
|
|
223
|
+
ihdr[11] = 0; // adaptive filtering
|
|
224
|
+
ihdr[12] = 0; // no interlace
|
|
225
|
+
var idat = zlib.deflateSync(filterRowsPaeth(data8, height, rowBytes, channels), {
|
|
226
|
+
level: DEFLATE_LEVEL,
|
|
227
|
+
});
|
|
228
|
+
return Buffer.concat([
|
|
229
|
+
Buffer.from(SIGNATURE),
|
|
230
|
+
chunk("IHDR", ihdr),
|
|
231
|
+
chunk("IDAT", idat),
|
|
232
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 16-bit samples are big-endian pairs, and the high byte *is* the 8-bit value:
|
|
237
|
+
// promoting 8-bit to 16-bit multiplies by 257 (0x7f -> 0x7f7f), so taking the
|
|
238
|
+
// high byte is exact for anything that started life at 8 bits and off by at most
|
|
239
|
+
// 1/255 for anything that did not. Rounding through /257 would cost a divide per
|
|
240
|
+
// sample and change nothing anybody looking at a screenshot could see.
|
|
241
|
+
function narrow16to8(data16, sampleCount) {
|
|
242
|
+
var out = Buffer.alloc(sampleCount);
|
|
243
|
+
for (var i = 0; i < sampleCount; i++) out[i] = data16[i * 2];
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function everyPixelTransparent(data8, channels, alphaIndex, pixelCount) {
|
|
248
|
+
for (var i = 0; i < pixelCount; i++) {
|
|
249
|
+
if (data8[i * channels + alphaIndex] !== 0) return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Normalise one PNG for delivery to an agent.
|
|
256
|
+
*
|
|
257
|
+
* Returns:
|
|
258
|
+
* buffer the PNG to send, or null when `empty` — an empty frame is
|
|
259
|
+
* reported, never shipped, so there is nothing to hand out
|
|
260
|
+
* width/height from IHDR, so they can never disagree with the pixels
|
|
261
|
+
* bitDepth of the *source*, before any conversion
|
|
262
|
+
* converted true when the bytes were re-encoded from 16-bit
|
|
263
|
+
* decoded true when the pixels were actually interpreted
|
|
264
|
+
* empty true when every pixel is fully transparent
|
|
265
|
+
* hashInput bytes to hash for the stale-render check
|
|
266
|
+
* hashBasis "pixels" when hashInput is decoded samples, "idat" when it is
|
|
267
|
+
* the compressed stream (metadata-free either way, so an
|
|
268
|
+
* embedded timestamp can never make two identical frames differ)
|
|
269
|
+
* passthrough why the pixels were not inspected, or null
|
|
270
|
+
*
|
|
271
|
+
* Throws on anything that is not a readable PNG. The caller is expected to treat
|
|
272
|
+
* that as "could not normalise", not as "the screenshot failed" — the render did
|
|
273
|
+
* happen, and shipping it unconverted with a warning beats discarding it.
|
|
274
|
+
*/
|
|
275
|
+
function normalizePng(buf) {
|
|
276
|
+
if (!Buffer.isBuffer(buf)) throw new Error("normalizePng expects a Buffer");
|
|
277
|
+
var chunks = readChunks(buf);
|
|
278
|
+
var h = parseHeader(chunks);
|
|
279
|
+
|
|
280
|
+
var idatParts = [];
|
|
281
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
282
|
+
if (chunks[i].type === "IDAT") idatParts.push(chunks[i].data);
|
|
283
|
+
}
|
|
284
|
+
if (!idatParts.length) throw new Error("malformed PNG: no IDAT chunk");
|
|
285
|
+
var idat = Buffer.concat(idatParts);
|
|
286
|
+
|
|
287
|
+
var channels = channelsFor(h.colorType);
|
|
288
|
+
var result = {
|
|
289
|
+
buffer: buf,
|
|
290
|
+
width: h.width,
|
|
291
|
+
height: h.height,
|
|
292
|
+
bitDepth: h.bitDepth,
|
|
293
|
+
colorType: h.colorType,
|
|
294
|
+
converted: false,
|
|
295
|
+
decoded: false,
|
|
296
|
+
empty: false,
|
|
297
|
+
hashInput: idat,
|
|
298
|
+
hashBasis: "idat",
|
|
299
|
+
passthrough: null,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (h.compression !== 0 || h.filter !== 0) {
|
|
303
|
+
throw new Error("unsupported PNG: compression " + h.compression + ", filter method " + h.filter);
|
|
304
|
+
}
|
|
305
|
+
if (channels === 0) throw new Error("unsupported PNG colour type " + h.colorType);
|
|
306
|
+
|
|
307
|
+
// Adam7 interlacing, palettes and sub-byte samples all need machinery that
|
|
308
|
+
// would be written blind for a case saveFrameToPng does not produce. Pass the
|
|
309
|
+
// file through untouched and record why the pixels were not read, rather than
|
|
310
|
+
// guessing at them — the stale check still works off the IDAT bytes.
|
|
311
|
+
if (h.interlace !== 0) {
|
|
312
|
+
result.passthrough = "interlaced";
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
if (h.colorType === 3) {
|
|
316
|
+
result.passthrough = "indexed colour";
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
if (h.bitDepth !== 8 && h.bitDepth !== 16) {
|
|
320
|
+
result.passthrough = h.bitDepth + "-bit samples";
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
var un = unfilter(zlib.inflateSync(idat), h.width, h.height, h.bitDepth, channels);
|
|
325
|
+
var pixels = h.bitDepth === 16
|
|
326
|
+
? narrow16to8(un.data, h.width * h.height * channels)
|
|
327
|
+
: un.data;
|
|
328
|
+
|
|
329
|
+
result.decoded = true;
|
|
330
|
+
result.hashInput = pixels;
|
|
331
|
+
result.hashBasis = "pixels";
|
|
332
|
+
|
|
333
|
+
var alphaIndex = alphaIndexFor(h.colorType);
|
|
334
|
+
if (alphaIndex >= 0 && everyPixelTransparent(pixels, channels, alphaIndex, h.width * h.height)) {
|
|
335
|
+
// Nothing to send, so nothing is encoded — and `buffer: null` makes it
|
|
336
|
+
// impossible for a caller to ship the frame by forgetting to check `empty`.
|
|
337
|
+
result.empty = true;
|
|
338
|
+
result.buffer = null;
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (h.bitDepth === 16) {
|
|
343
|
+
result.buffer = encodePng8(h.width, h.height, h.colorType, pixels);
|
|
344
|
+
result.converted = true;
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Decode a PNG to flat 8-bit samples.
|
|
351
|
+
*
|
|
352
|
+
* `normalizePng` deliberately passes the awkward encodings through untouched,
|
|
353
|
+
* because a screenshot it cannot read is still a screenshot the client might.
|
|
354
|
+
* A caller that is going to *resample* the pixels has no such fallback — half a
|
|
355
|
+
* decode is not a smaller picture, it is a wrong one — so this throws on
|
|
356
|
+
* anything it cannot interpret exactly.
|
|
357
|
+
*
|
|
358
|
+
* Returns { width, height, colorType, channels, bitDepth, pixels }, where
|
|
359
|
+
* `pixels` is width*height*channels bytes and `bitDepth` is the source's.
|
|
360
|
+
*/
|
|
361
|
+
function decodePng8(buf) {
|
|
362
|
+
if (!Buffer.isBuffer(buf)) throw new Error("decodePng8 expects a Buffer");
|
|
363
|
+
var chunks = readChunks(buf);
|
|
364
|
+
var h = parseHeader(chunks);
|
|
365
|
+
var channels = channelsFor(h.colorType);
|
|
366
|
+
|
|
367
|
+
if (h.compression !== 0 || h.filter !== 0) {
|
|
368
|
+
throw new Error("unsupported PNG: compression " + h.compression + ", filter method " + h.filter);
|
|
369
|
+
}
|
|
370
|
+
if (channels === 0) throw new Error("unsupported PNG colour type " + h.colorType);
|
|
371
|
+
if (h.interlace !== 0) throw new Error("unsupported PNG: interlaced");
|
|
372
|
+
if (h.colorType === 3) throw new Error("unsupported PNG: indexed colour");
|
|
373
|
+
if (h.bitDepth !== 8 && h.bitDepth !== 16) throw new Error("unsupported PNG: " + h.bitDepth + "-bit samples");
|
|
374
|
+
|
|
375
|
+
var idatParts = [];
|
|
376
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
377
|
+
if (chunks[i].type === "IDAT") idatParts.push(chunks[i].data);
|
|
378
|
+
}
|
|
379
|
+
if (!idatParts.length) throw new Error("malformed PNG: no IDAT chunk");
|
|
380
|
+
|
|
381
|
+
var un = unfilter(zlib.inflateSync(Buffer.concat(idatParts)), h.width, h.height, h.bitDepth, channels);
|
|
382
|
+
var pixels = h.bitDepth === 16 ? narrow16to8(un.data, h.width * h.height * channels) : un.data;
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
width: h.width,
|
|
386
|
+
height: h.height,
|
|
387
|
+
colorType: h.colorType,
|
|
388
|
+
channels: channels,
|
|
389
|
+
bitDepth: h.bitDepth,
|
|
390
|
+
pixels: pixels,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
normalizePng: normalizePng,
|
|
396
|
+
// For mogrt.js, which resamples a rendered frame into a template's thumbnail
|
|
397
|
+
// and needs the same decoder, encoder and CRC rather than a second copy of
|
|
398
|
+
// each. The zip format's CRC-32 is the one PNG uses, bit for bit.
|
|
399
|
+
decodePng8: decodePng8,
|
|
400
|
+
encodePng8: encodePng8,
|
|
401
|
+
crc32: crc32,
|
|
402
|
+
};
|