@engine-room/after-effects-mcp 0.3.1 → 0.4.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.
@@ -77,6 +77,120 @@ function crc32(bytes) {
77
77
  return (c ^ 0xffffffff) >>> 0;
78
78
  }
79
79
 
80
+ // ---------- Structural completeness ----------
81
+ /**
82
+ * Is this byte range a whole PNG file yet?
83
+ *
84
+ * Answered by walking the chunk table alone — no inflate, no pixels — because
85
+ * the caller is asking about a file After Effects may still be writing, and the
86
+ * question has to be cheap enough to ask repeatedly.
87
+ *
88
+ * It exists because "the file stopped growing" was never the same statement as
89
+ * "the file is finished". `saveFrameToPng` returns before the bytes are on disk,
90
+ * and the panel used to accept a frame the moment two `stat` calls 30ms apart
91
+ * reported the same size. A writer that pauses for 30ms — which on a heavy comp
92
+ * it routinely does — was read mid-flight and shipped, and the agent got
93
+ * `truncated PNG: chunk IDAT runs past the end of the file` for a render that
94
+ * was merely still happening. Issue #45.
95
+ *
96
+ * `growable` matters as much as `complete`: it separates "not finished yet,
97
+ * keep waiting" from "no number of further bytes can make this a PNG, stop
98
+ * now". A truncated chunk is the first; a wrong signature or a missing IHDR is
99
+ * the second, and waiting out a 120s render budget for one of those only turns
100
+ * a precise diagnosis into a timeout.
101
+ *
102
+ * Returns { complete, growable, reason, bytes, lastChunk, trailingBytes }.
103
+ */
104
+ function inspectPngStructure(buf) {
105
+ var n = buf.length;
106
+ var prefix = n < SIGNATURE.length ? n : SIGNATURE.length;
107
+ for (var i = 0; i < prefix; i++) {
108
+ if (buf[i] !== SIGNATURE[i]) {
109
+ return {
110
+ complete: false,
111
+ growable: false,
112
+ bytes: n,
113
+ reason: "not a PNG: signature mismatch at byte " + i,
114
+ };
115
+ }
116
+ }
117
+ if (n < SIGNATURE.length) {
118
+ return {
119
+ complete: false,
120
+ growable: true,
121
+ bytes: n,
122
+ reason: "only " + n + " of the 8 signature bytes are on disk",
123
+ };
124
+ }
125
+
126
+ var pos = SIGNATURE.length;
127
+ var first = true;
128
+ while (true) {
129
+ if (pos + 8 > n) {
130
+ return {
131
+ complete: false,
132
+ growable: true,
133
+ bytes: n,
134
+ reason: "the chunk header at byte " + pos + " is incomplete",
135
+ };
136
+ }
137
+ var len = buf.readUInt32BE(pos);
138
+ var type = buf.toString("ascii", pos + 4, pos + 8);
139
+ if (!/^[A-Za-z]{4}$/.test(type)) {
140
+ return {
141
+ complete: false,
142
+ growable: false,
143
+ bytes: n,
144
+ reason: "the chunk type at byte " + pos + " is not four letters",
145
+ };
146
+ }
147
+ // The format caps a chunk at 2^31-1. A larger figure is garbage, not a
148
+ // chunk still arriving, and calling it growable would spend the whole
149
+ // render budget waiting for a file that is already wrong.
150
+ if (len > 0x7fffffff) {
151
+ return {
152
+ complete: false,
153
+ growable: false,
154
+ bytes: n,
155
+ lastChunk: type,
156
+ reason: "chunk " + type + " declares " + len + " bytes, past the format's limit",
157
+ };
158
+ }
159
+ if (first && type !== "IHDR") {
160
+ return {
161
+ complete: false,
162
+ growable: false,
163
+ bytes: n,
164
+ lastChunk: type,
165
+ reason: "malformed PNG: the first chunk is " + type + ", not IHDR",
166
+ };
167
+ }
168
+ first = false;
169
+ var end = pos + 8 + len + 4;
170
+ if (end > n) {
171
+ return {
172
+ complete: false,
173
+ growable: true,
174
+ bytes: n,
175
+ lastChunk: type,
176
+ reason: "chunk " + type + " declares " + len + " bytes and " +
177
+ Math.max(0, n - pos - 8) + " of them are on disk",
178
+ };
179
+ }
180
+ if (type === "IEND") {
181
+ return {
182
+ complete: true,
183
+ growable: false,
184
+ bytes: n,
185
+ lastChunk: "IEND",
186
+ reason: null,
187
+ trailingBytes: n - end,
188
+ };
189
+ }
190
+ pos = end;
191
+ }
192
+ }
193
+
80
194
  // ---------- Reading ----------
81
195
  function readChunks(buf) {
82
196
  if (buf.length < 8) throw new Error("not a PNG: only " + buf.length + " bytes");
@@ -260,6 +374,7 @@ function everyPixelTransparent(data8, channels, alphaIndex, pixelCount) {
260
374
  * width/height from IHDR, so they can never disagree with the pixels
261
375
  * bitDepth of the *source*, before any conversion
262
376
  * converted true when the bytes were re-encoded from 16-bit
377
+ * channels samples per pixel, for a caller that composites hashInput
263
378
  * decoded true when the pixels were actually interpreted
264
379
  * empty true when every pixel is fully transparent
265
380
  * hashInput bytes to hash for the stale-render check
@@ -291,6 +406,9 @@ function normalizePng(buf) {
291
406
  height: h.height,
292
407
  bitDepth: h.bitDepth,
293
408
  colorType: h.colorType,
409
+ // Samples per pixel, so a caller compositing `hashInput` into a contact
410
+ // sheet does not have to re-derive it from the colour type.
411
+ channels: channels,
294
412
  converted: false,
295
413
  decoded: false,
296
414
  empty: false,
@@ -393,6 +511,10 @@ function decodePng8(buf) {
393
511
 
394
512
  module.exports = {
395
513
  normalizePng: normalizePng,
514
+ // For main.js, which has to know whether the file After Effects is writing is
515
+ // finished before it reads it. It lives here because the chunk walk and the
516
+ // signature it walks from are already defined once, above.
517
+ inspectPngStructure: inspectPngStructure,
396
518
  // For mogrt.js, which resamples a rendered frame into a template's thumbnail
397
519
  // and needs the same decoder, encoder and CRC rather than a second copy of
398
520
  // each. The zip format's CRC-32 is the one PNG uses, bit for bit.