@standardagents/sip 0.13.1 → 1.0.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 +44 -8
- package/dist/index.d.ts +84 -525
- package/dist/index.js +1388 -651
- package/dist/index.js.map +1 -1
- package/dist/sip.js +1 -1
- package/dist/sip.wasm +0 -0
- package/package.json +35 -17
- package/LICENSE.txt +0 -48
package/dist/index.js
CHANGED
|
@@ -1,3 +1,145 @@
|
|
|
1
|
+
// src/decoders/simple.ts
|
|
2
|
+
function isCloudflareWorker() {
|
|
3
|
+
const cacheStorage = globalThis.caches;
|
|
4
|
+
return typeof cacheStorage !== "undefined" && typeof cacheStorage.default !== "undefined";
|
|
5
|
+
}
|
|
6
|
+
function getPreloadedCodecBinary(format) {
|
|
7
|
+
const globalValue = globalThis.__SIP_CODEC_WASM__;
|
|
8
|
+
if (!globalValue || typeof globalValue !== "object") {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const formatValue = globalValue[format];
|
|
12
|
+
if (formatValue instanceof ArrayBuffer || formatValue instanceof Uint8Array || formatValue instanceof WebAssembly.Module) {
|
|
13
|
+
return formatValue;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function isNode() {
|
|
18
|
+
if (isCloudflareWorker()) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
22
|
+
}
|
|
23
|
+
async function initCodecWithBinary(initFn, wasmSource) {
|
|
24
|
+
if (wasmSource instanceof WebAssembly.Module) {
|
|
25
|
+
await initFn(wasmSource);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let buffer;
|
|
29
|
+
if (wasmSource instanceof Uint8Array) {
|
|
30
|
+
const copy = new Uint8Array(wasmSource.byteLength);
|
|
31
|
+
copy.set(wasmSource);
|
|
32
|
+
buffer = copy.buffer;
|
|
33
|
+
} else {
|
|
34
|
+
buffer = wasmSource;
|
|
35
|
+
}
|
|
36
|
+
const wasmModule2 = await WebAssembly.compile(buffer);
|
|
37
|
+
await initFn(wasmModule2);
|
|
38
|
+
}
|
|
39
|
+
async function initCodecForNode(initFn, wasmPath) {
|
|
40
|
+
const { readFile } = await import('fs/promises');
|
|
41
|
+
const { createRequire } = await import('module');
|
|
42
|
+
const require2 = createRequire(import.meta.url);
|
|
43
|
+
const resolvedPath = require2.resolve(wasmPath);
|
|
44
|
+
const wasmBuffer = await readFile(resolvedPath);
|
|
45
|
+
const wasmModule2 = await WebAssembly.compile(wasmBuffer);
|
|
46
|
+
await initFn(wasmModule2);
|
|
47
|
+
}
|
|
48
|
+
var SimpleDecoder = class {
|
|
49
|
+
format;
|
|
50
|
+
supportsScanline = false;
|
|
51
|
+
supportsScaledDecode = false;
|
|
52
|
+
data;
|
|
53
|
+
width = 0;
|
|
54
|
+
height = 0;
|
|
55
|
+
hasAlpha = false;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
decodeFn = null;
|
|
58
|
+
constructor(format, data) {
|
|
59
|
+
this.format = format;
|
|
60
|
+
this.data = data;
|
|
61
|
+
}
|
|
62
|
+
async init(data) {
|
|
63
|
+
this.data = data;
|
|
64
|
+
switch (this.format) {
|
|
65
|
+
case "avif": {
|
|
66
|
+
const { default: decode2, init } = await import('@jsquash/avif/decode.js');
|
|
67
|
+
const preloaded = getPreloadedCodecBinary("avif");
|
|
68
|
+
if (preloaded) {
|
|
69
|
+
await initCodecWithBinary(init, preloaded);
|
|
70
|
+
} else if (isNode()) {
|
|
71
|
+
await initCodecForNode(init, "@jsquash/avif/codec/dec/avif_dec.wasm");
|
|
72
|
+
}
|
|
73
|
+
this.decodeFn = decode2;
|
|
74
|
+
this.hasAlpha = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "webp": {
|
|
78
|
+
const { default: decode2, init } = await import('@jsquash/webp/decode.js');
|
|
79
|
+
const preloaded = getPreloadedCodecBinary("webp");
|
|
80
|
+
if (preloaded) {
|
|
81
|
+
await initCodecWithBinary(init, preloaded);
|
|
82
|
+
} else if (isNode()) {
|
|
83
|
+
await initCodecForNode(init, "@jsquash/webp/codec/dec/webp_dec.wasm");
|
|
84
|
+
}
|
|
85
|
+
this.decodeFn = decode2;
|
|
86
|
+
this.hasAlpha = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "jpeg":
|
|
90
|
+
case "png":
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${this.format.toUpperCase()} requires native WASM decoder. Build the WASM module with \`pnpm build:wasm\` in the @standardagents/sip repo root.`
|
|
93
|
+
);
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unsupported format for SimpleDecoder: ${this.format}`);
|
|
96
|
+
}
|
|
97
|
+
const imageData = await this.decodeFn(this.data);
|
|
98
|
+
if (!imageData) {
|
|
99
|
+
throw new Error(`Failed to decode ${this.format} image`);
|
|
100
|
+
}
|
|
101
|
+
this.width = imageData.width;
|
|
102
|
+
this.height = imageData.height;
|
|
103
|
+
return {
|
|
104
|
+
width: this.width,
|
|
105
|
+
height: this.height,
|
|
106
|
+
hasAlpha: this.hasAlpha
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async decode(_scaleFactor) {
|
|
110
|
+
if (!this.decodeFn) {
|
|
111
|
+
throw new Error("Decoder not initialized. Call init() first.");
|
|
112
|
+
}
|
|
113
|
+
const imageData = await this.decodeFn(this.data);
|
|
114
|
+
this.width = imageData.width;
|
|
115
|
+
this.height = imageData.height;
|
|
116
|
+
const rgba = new Uint8Array(imageData.data.buffer);
|
|
117
|
+
const rgb = new Uint8Array(this.width * this.height * 3);
|
|
118
|
+
let srcIdx = 0;
|
|
119
|
+
let dstIdx = 0;
|
|
120
|
+
const pixelCount = this.width * this.height;
|
|
121
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
122
|
+
rgb[dstIdx++] = rgba[srcIdx++];
|
|
123
|
+
rgb[dstIdx++] = rgba[srcIdx++];
|
|
124
|
+
rgb[dstIdx++] = rgba[srcIdx++];
|
|
125
|
+
srcIdx++;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
pixels: rgb,
|
|
129
|
+
width: this.width,
|
|
130
|
+
height: this.height
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
dispose() {
|
|
134
|
+
this.decodeFn = null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
async function createDecoder(format, data) {
|
|
138
|
+
const decoder = new SimpleDecoder(format, data);
|
|
139
|
+
await decoder.init(data);
|
|
140
|
+
return decoder;
|
|
141
|
+
}
|
|
142
|
+
|
|
1
143
|
// src/probe.ts
|
|
2
144
|
var MAGIC = {
|
|
3
145
|
// JPEG: FFD8FF
|
|
@@ -158,155 +300,401 @@ function probe(input) {
|
|
|
158
300
|
hasAlpha: result.hasAlpha ?? false
|
|
159
301
|
};
|
|
160
302
|
}
|
|
161
|
-
function detectImageFormat(input) {
|
|
162
|
-
const data = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
|
|
163
|
-
return detectFormat(data);
|
|
164
|
-
}
|
|
165
303
|
|
|
166
|
-
// src/
|
|
167
|
-
|
|
168
|
-
|
|
304
|
+
// src/input.ts
|
|
305
|
+
var INSPECT_TARGETS = [64, 512, 4096, 16384, 65536, 262144];
|
|
306
|
+
var STREAM_CHUNK_TARGET = 64 * 1024;
|
|
307
|
+
function sliceArrayBuffer(view) {
|
|
308
|
+
const copy = new Uint8Array(view.byteLength);
|
|
309
|
+
copy.set(view);
|
|
310
|
+
return copy.buffer;
|
|
169
311
|
}
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const wasmModule2 = await WebAssembly.compile(wasmBuffer);
|
|
177
|
-
await initFn(wasmModule2);
|
|
178
|
-
}
|
|
179
|
-
var SimpleDecoder = class {
|
|
180
|
-
format;
|
|
181
|
-
supportsScanline = false;
|
|
182
|
-
supportsScaledDecode = false;
|
|
183
|
-
data;
|
|
184
|
-
width = 0;
|
|
185
|
-
height = 0;
|
|
186
|
-
hasAlpha = false;
|
|
187
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
-
decodeFn = null;
|
|
189
|
-
constructor(format, data) {
|
|
190
|
-
this.format = format;
|
|
191
|
-
this.data = data;
|
|
312
|
+
function concatChunks(chunks, total) {
|
|
313
|
+
const merged = new Uint8Array(total);
|
|
314
|
+
let offset = 0;
|
|
315
|
+
for (const chunk of chunks) {
|
|
316
|
+
merged.set(chunk, offset);
|
|
317
|
+
offset += chunk.byteLength;
|
|
192
318
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
319
|
+
return merged;
|
|
320
|
+
}
|
|
321
|
+
function normalizeChunk(chunk) {
|
|
322
|
+
if (chunk.byteOffset === 0 && chunk.byteLength === chunk.buffer.byteLength) {
|
|
323
|
+
const copy2 = new Uint8Array(chunk.byteLength);
|
|
324
|
+
copy2.set(chunk);
|
|
325
|
+
return copy2;
|
|
326
|
+
}
|
|
327
|
+
const copy = new Uint8Array(chunk.byteLength);
|
|
328
|
+
copy.set(chunk);
|
|
329
|
+
return copy;
|
|
330
|
+
}
|
|
331
|
+
async function* iterateReadableStream(stream) {
|
|
332
|
+
const reader = stream.getReader();
|
|
333
|
+
try {
|
|
334
|
+
while (true) {
|
|
335
|
+
const { value, done } = await reader.read();
|
|
336
|
+
if (done) {
|
|
337
|
+
return;
|
|
204
338
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (isNode()) {
|
|
208
|
-
await initCodecForNode(init, "@jsquash/webp/codec/dec/webp_dec.wasm");
|
|
209
|
-
}
|
|
210
|
-
this.decodeFn = decode;
|
|
211
|
-
this.hasAlpha = true;
|
|
212
|
-
break;
|
|
339
|
+
if (value && value.byteLength > 0) {
|
|
340
|
+
yield normalizeChunk(value);
|
|
213
341
|
}
|
|
214
|
-
case "jpeg":
|
|
215
|
-
case "png":
|
|
216
|
-
throw new Error(
|
|
217
|
-
`${this.format.toUpperCase()} requires native WASM decoder. Build the WASM module with \`pnpm build:wasm\` in packages/sip.`
|
|
218
|
-
);
|
|
219
|
-
default:
|
|
220
|
-
throw new Error(`Unsupported format for SimpleDecoder: ${this.format}`);
|
|
221
|
-
}
|
|
222
|
-
const imageData = await this.decodeFn(this.data);
|
|
223
|
-
if (!imageData) {
|
|
224
|
-
throw new Error(`Failed to decode ${this.format} image`);
|
|
225
342
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
width: this.width,
|
|
230
|
-
height: this.height,
|
|
231
|
-
hasAlpha: this.hasAlpha
|
|
232
|
-
};
|
|
343
|
+
} finally {
|
|
344
|
+
reader.releaseLock();
|
|
233
345
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
346
|
+
}
|
|
347
|
+
async function* coalesceAsyncIterable(input, target = STREAM_CHUNK_TARGET) {
|
|
348
|
+
let pending = [];
|
|
349
|
+
let total = 0;
|
|
350
|
+
const flush = () => {
|
|
351
|
+
if (total === 0) {
|
|
352
|
+
return null;
|
|
237
353
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
354
|
+
const merged2 = concatChunks(pending, total);
|
|
355
|
+
pending = [];
|
|
356
|
+
total = 0;
|
|
357
|
+
return merged2;
|
|
358
|
+
};
|
|
359
|
+
for await (const rawChunk of input) {
|
|
360
|
+
const chunk = normalizeChunk(rawChunk);
|
|
361
|
+
if (chunk.byteLength >= target && total === 0) {
|
|
362
|
+
yield chunk;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
pending.push(chunk);
|
|
366
|
+
total += chunk.byteLength;
|
|
367
|
+
if (total >= target) {
|
|
368
|
+
const merged2 = flush();
|
|
369
|
+
if (merged2) {
|
|
370
|
+
yield merged2;
|
|
371
|
+
}
|
|
251
372
|
}
|
|
252
|
-
return {
|
|
253
|
-
pixels: rgb,
|
|
254
|
-
width: this.width,
|
|
255
|
-
height: this.height
|
|
256
|
-
};
|
|
257
373
|
}
|
|
258
|
-
|
|
259
|
-
|
|
374
|
+
const merged = flush();
|
|
375
|
+
if (merged) {
|
|
376
|
+
yield merged;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function getAsyncIterable(input) {
|
|
380
|
+
if (typeof ReadableStream !== "undefined" && input instanceof ReadableStream) {
|
|
381
|
+
return coalesceAsyncIterable(iterateReadableStream(input));
|
|
382
|
+
}
|
|
383
|
+
if (typeof input[Symbol.asyncIterator] === "function") {
|
|
384
|
+
return coalesceAsyncIterable(input);
|
|
385
|
+
}
|
|
386
|
+
return coalesceAsyncIterable(input);
|
|
387
|
+
}
|
|
388
|
+
var BytesInputSource = class {
|
|
389
|
+
constructor(bytes, formatHint) {
|
|
390
|
+
this.bytes = bytes;
|
|
391
|
+
this.byteLength = bytes.byteLength;
|
|
392
|
+
this.headerBytes = bytes.subarray(0, Math.min(bytes.byteLength, INSPECT_TARGETS.at(-1)));
|
|
393
|
+
this.formatHint = formatHint;
|
|
394
|
+
}
|
|
395
|
+
bytes;
|
|
396
|
+
kind = "bytes";
|
|
397
|
+
replayable = true;
|
|
398
|
+
byteLength;
|
|
399
|
+
formatHint;
|
|
400
|
+
headerBytes;
|
|
401
|
+
done = true;
|
|
402
|
+
async ensureHeaderBytes(target) {
|
|
403
|
+
return this.bytes.subarray(0, Math.min(this.bytes.byteLength, target));
|
|
404
|
+
}
|
|
405
|
+
open() {
|
|
406
|
+
const bytes = this.bytes;
|
|
407
|
+
return (async function* openBytes() {
|
|
408
|
+
yield bytes;
|
|
409
|
+
})();
|
|
260
410
|
}
|
|
261
411
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
412
|
+
var StreamInputSource = class {
|
|
413
|
+
kind = "stream";
|
|
414
|
+
replayable = false;
|
|
415
|
+
byteLength;
|
|
416
|
+
formatHint;
|
|
417
|
+
iterator;
|
|
418
|
+
peekedChunks = [];
|
|
419
|
+
peekedBytes = 0;
|
|
420
|
+
opened = false;
|
|
421
|
+
exhausted = false;
|
|
422
|
+
headerBytes = new Uint8Array(0);
|
|
423
|
+
constructor(input, formatHint, byteLength) {
|
|
424
|
+
this.iterator = input[Symbol.asyncIterator]();
|
|
425
|
+
this.formatHint = formatHint;
|
|
426
|
+
this.byteLength = byteLength;
|
|
427
|
+
}
|
|
428
|
+
get done() {
|
|
429
|
+
return this.exhausted;
|
|
430
|
+
}
|
|
431
|
+
async ensureHeaderBytes(target) {
|
|
432
|
+
while (!this.exhausted && this.peekedBytes < target) {
|
|
433
|
+
const { value, done } = await this.iterator.next();
|
|
434
|
+
if (done) {
|
|
435
|
+
this.exhausted = true;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
if (value && value.byteLength > 0) {
|
|
439
|
+
const chunk = normalizeChunk(value);
|
|
440
|
+
this.peekedChunks.push(chunk);
|
|
441
|
+
this.peekedBytes += chunk.byteLength;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
this.headerBytes = concatChunks(this.peekedChunks, this.peekedBytes);
|
|
445
|
+
return this.headerBytes;
|
|
446
|
+
}
|
|
447
|
+
open() {
|
|
448
|
+
if (this.opened) {
|
|
449
|
+
throw new Error("Input source can only be opened once");
|
|
450
|
+
}
|
|
451
|
+
this.opened = true;
|
|
452
|
+
const replay = this.peekedChunks.slice();
|
|
453
|
+
const iterator = this.iterator;
|
|
454
|
+
return (async function* openStream() {
|
|
455
|
+
for (const chunk of replay) {
|
|
456
|
+
yield chunk;
|
|
457
|
+
}
|
|
458
|
+
while (true) {
|
|
459
|
+
const { value, done } = await iterator.next();
|
|
460
|
+
if (done) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (value && value.byteLength > 0) {
|
|
464
|
+
yield normalizeChunk(value);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
})();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
function isInputSource(value) {
|
|
471
|
+
return typeof value === "object" && value !== null && "open" in value && "headerBytes" in value;
|
|
266
472
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
var wasmModule = null;
|
|
270
|
-
var wasmPromise = null;
|
|
271
|
-
var precompiledWasmModule = null;
|
|
272
|
-
function isWasmAvailable() {
|
|
273
|
-
return wasmModule !== null;
|
|
473
|
+
function toUint8Array(input) {
|
|
474
|
+
return input instanceof Uint8Array ? normalizeChunk(input) : new Uint8Array(input);
|
|
274
475
|
}
|
|
275
|
-
async function
|
|
276
|
-
|
|
277
|
-
|
|
476
|
+
async function sourceFromRequestLike(input) {
|
|
477
|
+
const contentType = input.headers.get("content-type") ?? "";
|
|
478
|
+
const hint = contentType.startsWith("image/") ? contentType.slice("image/".length) : void 0;
|
|
479
|
+
const lengthHeader = input.headers.get("content-length");
|
|
480
|
+
const byteLength = lengthHeader ? Number(lengthHeader) : void 0;
|
|
481
|
+
if (input.body) {
|
|
482
|
+
return new StreamInputSource(getAsyncIterable(input.body), hint, Number.isFinite(byteLength) ? byteLength : void 0);
|
|
483
|
+
}
|
|
484
|
+
const bytes = new Uint8Array(await input.arrayBuffer());
|
|
485
|
+
return new BytesInputSource(bytes, hint);
|
|
486
|
+
}
|
|
487
|
+
async function prepareInputSource(input) {
|
|
488
|
+
if (isInputSource(input)) {
|
|
489
|
+
return input;
|
|
278
490
|
}
|
|
279
|
-
if (
|
|
280
|
-
|
|
491
|
+
if (input instanceof ArrayBuffer || input instanceof Uint8Array) {
|
|
492
|
+
return new BytesInputSource(toUint8Array(input));
|
|
281
493
|
}
|
|
282
|
-
|
|
494
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
495
|
+
return new BytesInputSource(new Uint8Array(await input.arrayBuffer()));
|
|
496
|
+
}
|
|
497
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
498
|
+
return sourceFromRequestLike(input);
|
|
499
|
+
}
|
|
500
|
+
if (typeof Response !== "undefined" && input instanceof Response) {
|
|
501
|
+
return sourceFromRequestLike(input);
|
|
502
|
+
}
|
|
503
|
+
if (typeof ReadableStream !== "undefined" && input instanceof ReadableStream) {
|
|
504
|
+
return new StreamInputSource(getAsyncIterable(input));
|
|
505
|
+
}
|
|
506
|
+
return new StreamInputSource(getAsyncIterable(input));
|
|
283
507
|
}
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
508
|
+
async function inspect(input) {
|
|
509
|
+
const source = await prepareInputSource(input);
|
|
510
|
+
const info = await inspectSource(source);
|
|
511
|
+
if (info.format === "unknown") {
|
|
512
|
+
throw new Error("Unsupported image format");
|
|
287
513
|
}
|
|
288
|
-
return
|
|
514
|
+
return { info, source };
|
|
289
515
|
}
|
|
290
|
-
async function
|
|
291
|
-
|
|
292
|
-
|
|
516
|
+
async function inspectSource(source) {
|
|
517
|
+
let best = probe(source.headerBytes);
|
|
518
|
+
if (best.format !== "unknown") {
|
|
519
|
+
return best;
|
|
293
520
|
}
|
|
294
|
-
|
|
295
|
-
|
|
521
|
+
for (const target of INSPECT_TARGETS) {
|
|
522
|
+
const bytes = await source.ensureHeaderBytes(target);
|
|
523
|
+
best = probe(bytes);
|
|
524
|
+
if (best.format !== "unknown") {
|
|
525
|
+
return best;
|
|
526
|
+
}
|
|
296
527
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
wasmModule = await wasmPromise;
|
|
300
|
-
return wasmModule;
|
|
301
|
-
} catch (err) {
|
|
302
|
-
wasmPromise = null;
|
|
303
|
-
throw err;
|
|
528
|
+
if (source.headerBytes.byteLength === 0) {
|
|
529
|
+
return { format: "unknown", width: 0, height: 0, hasAlpha: false };
|
|
304
530
|
}
|
|
531
|
+
return probe(source.headerBytes);
|
|
305
532
|
}
|
|
306
|
-
async function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
533
|
+
async function collectSourceBytes(source) {
|
|
534
|
+
const chunks = [];
|
|
535
|
+
let total = 0;
|
|
536
|
+
for await (const chunk of source.open()) {
|
|
537
|
+
chunks.push(chunk);
|
|
538
|
+
total += chunk.byteLength;
|
|
539
|
+
}
|
|
540
|
+
return concatChunks(chunks, total);
|
|
541
|
+
}
|
|
542
|
+
function asArrayBuffer(bytes) {
|
|
543
|
+
return sliceArrayBuffer(bytes);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/resize.ts
|
|
547
|
+
function createResizeState(srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
548
|
+
return {
|
|
549
|
+
srcWidth,
|
|
550
|
+
srcHeight,
|
|
551
|
+
dstWidth,
|
|
552
|
+
dstHeight,
|
|
553
|
+
bufferA: null,
|
|
554
|
+
bufferB: null,
|
|
555
|
+
bufferAY: -1,
|
|
556
|
+
bufferBY: -1,
|
|
557
|
+
currentOutputY: 0
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function resizeRowHorizontal(src, srcWidth, dstWidth) {
|
|
561
|
+
const dst = new Uint8Array(dstWidth * 3);
|
|
562
|
+
const xScale = srcWidth / dstWidth;
|
|
563
|
+
for (let dstX = 0; dstX < dstWidth; dstX++) {
|
|
564
|
+
const srcXFloat = dstX * xScale;
|
|
565
|
+
const srcX0 = Math.floor(srcXFloat);
|
|
566
|
+
const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);
|
|
567
|
+
const t = srcXFloat - srcX0;
|
|
568
|
+
const invT = 1 - t;
|
|
569
|
+
const src0 = srcX0 * 3;
|
|
570
|
+
const src1 = srcX1 * 3;
|
|
571
|
+
const dstOffset = dstX * 3;
|
|
572
|
+
dst[dstOffset] = Math.round(src[src0] * invT + src[src1] * t);
|
|
573
|
+
dst[dstOffset + 1] = Math.round(src[src0 + 1] * invT + src[src1 + 1] * t);
|
|
574
|
+
dst[dstOffset + 2] = Math.round(src[src0 + 2] * invT + src[src1 + 2] * t);
|
|
575
|
+
}
|
|
576
|
+
return dst;
|
|
577
|
+
}
|
|
578
|
+
function blendRows(rowA, rowB, t, width) {
|
|
579
|
+
const result = new Uint8Array(width * 3);
|
|
580
|
+
const invT = 1 - t;
|
|
581
|
+
for (let i = 0; i < width * 3; i++) {
|
|
582
|
+
result[i] = Math.round(rowA[i] * invT + rowB[i] * t);
|
|
583
|
+
}
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
function processScanline(state, srcScanline, srcY) {
|
|
587
|
+
const { srcWidth, srcHeight, dstWidth, dstHeight } = state;
|
|
588
|
+
const yScale = srcHeight / dstHeight;
|
|
589
|
+
const output = [];
|
|
590
|
+
const resizedRow = resizeRowHorizontal(srcScanline, srcWidth, dstWidth);
|
|
591
|
+
state.bufferA = state.bufferB;
|
|
592
|
+
state.bufferAY = state.bufferBY;
|
|
593
|
+
state.bufferB = resizedRow;
|
|
594
|
+
state.bufferBY = srcY;
|
|
595
|
+
while (state.currentOutputY < dstHeight) {
|
|
596
|
+
const srcYFloat = state.currentOutputY * yScale;
|
|
597
|
+
const srcYFloor = Math.floor(srcYFloat);
|
|
598
|
+
const srcYCeil = Math.min(srcYFloor + 1, srcHeight - 1);
|
|
599
|
+
if (srcYCeil > srcY) {
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
if (state.bufferA === null) {
|
|
603
|
+
output.push({
|
|
604
|
+
data: state.bufferB,
|
|
605
|
+
width: dstWidth,
|
|
606
|
+
y: state.currentOutputY
|
|
607
|
+
});
|
|
608
|
+
state.currentOutputY++;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const t = srcYFloat - srcYFloor;
|
|
612
|
+
let rowA = state.bufferA;
|
|
613
|
+
let rowB = state.bufferB;
|
|
614
|
+
if (srcYFloor === state.bufferBY) {
|
|
615
|
+
rowA = state.bufferB;
|
|
616
|
+
rowB = state.bufferB;
|
|
617
|
+
} else if (srcYCeil === state.bufferAY) {
|
|
618
|
+
rowA = state.bufferA;
|
|
619
|
+
rowB = state.bufferA;
|
|
620
|
+
}
|
|
621
|
+
const blended = blendRows(rowA, rowB, t, dstWidth);
|
|
622
|
+
output.push({
|
|
623
|
+
data: blended,
|
|
624
|
+
width: dstWidth,
|
|
625
|
+
y: state.currentOutputY
|
|
626
|
+
});
|
|
627
|
+
state.currentOutputY++;
|
|
628
|
+
}
|
|
629
|
+
return output;
|
|
630
|
+
}
|
|
631
|
+
function flushResize(state) {
|
|
632
|
+
const output = [];
|
|
633
|
+
while (state.currentOutputY < state.dstHeight) {
|
|
634
|
+
if (state.bufferB === null) break;
|
|
635
|
+
output.push({
|
|
636
|
+
data: state.bufferB,
|
|
637
|
+
width: state.dstWidth,
|
|
638
|
+
y: state.currentOutputY
|
|
639
|
+
});
|
|
640
|
+
state.currentOutputY++;
|
|
641
|
+
}
|
|
642
|
+
return output;
|
|
643
|
+
}
|
|
644
|
+
function calculateTargetDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
|
|
645
|
+
const scaleX = maxWidth / srcWidth;
|
|
646
|
+
const scaleY = maxHeight / srcHeight;
|
|
647
|
+
const scale = Math.min(scaleX, scaleY, 1);
|
|
648
|
+
return {
|
|
649
|
+
width: Math.round(srcWidth * scale),
|
|
650
|
+
height: Math.round(srcHeight * scale),
|
|
651
|
+
scale
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/wasm/loader.ts
|
|
656
|
+
var wasmModule = null;
|
|
657
|
+
var wasmPromise = null;
|
|
658
|
+
var precompiledWasmModule = null;
|
|
659
|
+
function isCloudflareWorker2() {
|
|
660
|
+
const cacheStorage = globalThis.caches;
|
|
661
|
+
return typeof cacheStorage !== "undefined" && typeof cacheStorage.default !== "undefined";
|
|
662
|
+
}
|
|
663
|
+
async function initWithWasmModule(compiledModule) {
|
|
664
|
+
if (wasmModule) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (compiledModule) {
|
|
668
|
+
precompiledWasmModule = compiledModule;
|
|
669
|
+
}
|
|
670
|
+
await loadWasm();
|
|
671
|
+
}
|
|
672
|
+
function getWasmModule() {
|
|
673
|
+
if (!wasmModule) {
|
|
674
|
+
throw new Error("WASM module not loaded. Call loadWasm() first.");
|
|
675
|
+
}
|
|
676
|
+
return wasmModule;
|
|
677
|
+
}
|
|
678
|
+
async function loadWasm() {
|
|
679
|
+
if (wasmModule) {
|
|
680
|
+
return wasmModule;
|
|
681
|
+
}
|
|
682
|
+
if (wasmPromise) {
|
|
683
|
+
return wasmPromise;
|
|
684
|
+
}
|
|
685
|
+
wasmPromise = doLoadWasm();
|
|
686
|
+
try {
|
|
687
|
+
wasmModule = await wasmPromise;
|
|
688
|
+
return wasmModule;
|
|
689
|
+
} catch (err) {
|
|
690
|
+
wasmPromise = null;
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function doLoadWasm() {
|
|
695
|
+
if (typeof globalThis !== "undefined" && globalThis.__SIP_WASM_LOADER__) {
|
|
696
|
+
const loader = globalThis.__SIP_WASM_LOADER__;
|
|
697
|
+
return await loader();
|
|
310
698
|
}
|
|
311
699
|
try {
|
|
312
700
|
const createSipModule = (await import('./sip.js')).default;
|
|
@@ -336,11 +724,18 @@ async function doLoadWasm() {
|
|
|
336
724
|
});
|
|
337
725
|
return module2;
|
|
338
726
|
}
|
|
727
|
+
const isNode2 = !isCloudflareWorker2() && typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
728
|
+
if (isNode2) {
|
|
729
|
+
const { readFile } = await import('fs/promises');
|
|
730
|
+
const wasmBinary = await readFile(new URL("./sip.wasm", import.meta.url));
|
|
731
|
+
const module2 = await createSipModule({ wasmBinary });
|
|
732
|
+
return module2;
|
|
733
|
+
}
|
|
339
734
|
const module = await createSipModule();
|
|
340
735
|
return module;
|
|
341
736
|
} catch (err) {
|
|
342
737
|
throw new Error(
|
|
343
|
-
"SIP WASM module not available. To use streaming processing, build the WASM module with `pnpm build:wasm` in
|
|
738
|
+
"SIP WASM module not available. To use streaming processing, build the WASM module with `pnpm build:wasm` in the @standardagents/sip repo root. Error: " + (err instanceof Error ? err.message : String(err))
|
|
344
739
|
);
|
|
345
740
|
}
|
|
346
741
|
}
|
|
@@ -360,175 +755,161 @@ function copyFromWasm(module, ptr, size) {
|
|
|
360
755
|
var WasmJpegDecoder = class {
|
|
361
756
|
module;
|
|
362
757
|
decoder = 0;
|
|
363
|
-
dataPtr = 0;
|
|
364
758
|
width = 0;
|
|
365
759
|
height = 0;
|
|
366
760
|
outputWidth = 0;
|
|
367
761
|
outputHeight = 0;
|
|
368
|
-
scaleDenom = 1;
|
|
369
762
|
rowBufferPtr = 0;
|
|
370
763
|
started = false;
|
|
371
764
|
finished = false;
|
|
372
765
|
constructor() {
|
|
373
766
|
this.module = getWasmModule();
|
|
767
|
+
this.decoder = this.module._sip_decoder_create();
|
|
768
|
+
if (!this.decoder) {
|
|
769
|
+
throw new Error("Failed to create JPEG decoder");
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
pushInput(data, isFinal = false) {
|
|
773
|
+
if (data.byteLength === 0 && !isFinal) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let ptr = 0;
|
|
777
|
+
try {
|
|
778
|
+
ptr = data.byteLength > 0 ? copyToWasm(this.module, data) : 0;
|
|
779
|
+
if (this.module._sip_decoder_push_input(this.decoder, ptr, data.byteLength, isFinal ? 1 : 0) !== 0) {
|
|
780
|
+
throw new Error("Failed to feed JPEG bytes into decoder");
|
|
781
|
+
}
|
|
782
|
+
} finally {
|
|
783
|
+
if (ptr) {
|
|
784
|
+
this.module._free(ptr);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
374
787
|
}
|
|
375
788
|
/**
|
|
376
|
-
*
|
|
789
|
+
* Compatibility helper for full-buffer callers.
|
|
377
790
|
*/
|
|
378
791
|
init(data) {
|
|
379
|
-
const bytes = data instanceof
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
792
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
793
|
+
let ptr = 0;
|
|
794
|
+
try {
|
|
795
|
+
ptr = copyToWasm(this.module, bytes);
|
|
796
|
+
if (this.module._sip_decoder_set_source(this.decoder, ptr, bytes.byteLength) !== 0) {
|
|
797
|
+
throw new Error("Failed to set buffered JPEG source");
|
|
798
|
+
}
|
|
799
|
+
} finally {
|
|
800
|
+
if (ptr) {
|
|
801
|
+
this.module._free(ptr);
|
|
802
|
+
}
|
|
383
803
|
}
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
throw new Error("Failed to set decoder source");
|
|
804
|
+
const header = this.readHeaderStep();
|
|
805
|
+
if (header !== "ready") {
|
|
806
|
+
throw new Error("Incomplete JPEG header");
|
|
388
807
|
}
|
|
389
|
-
|
|
390
|
-
|
|
808
|
+
return { width: this.width, height: this.height };
|
|
809
|
+
}
|
|
810
|
+
readHeaderStep() {
|
|
811
|
+
const result = this.module._sip_decoder_read_header(this.decoder);
|
|
812
|
+
if (result === 1) {
|
|
813
|
+
return "needMore";
|
|
814
|
+
}
|
|
815
|
+
if (result !== 0) {
|
|
391
816
|
throw new Error("Failed to read JPEG header");
|
|
392
817
|
}
|
|
393
818
|
this.width = this.module._sip_decoder_get_width(this.decoder);
|
|
394
819
|
this.height = this.module._sip_decoder_get_height(this.decoder);
|
|
395
820
|
this.outputWidth = this.width;
|
|
396
821
|
this.outputHeight = this.height;
|
|
397
|
-
return
|
|
822
|
+
return "ready";
|
|
398
823
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Get original image dimensions
|
|
401
|
-
*/
|
|
402
824
|
getDimensions() {
|
|
403
825
|
return { width: this.width, height: this.height };
|
|
404
826
|
}
|
|
405
|
-
/**
|
|
406
|
-
* Set DCT scale factor for decoding
|
|
407
|
-
*
|
|
408
|
-
* Must be called after init() and before start()
|
|
409
|
-
*
|
|
410
|
-
* @param scaleDenom - Scale denominator: 1, 2, 4, or 8
|
|
411
|
-
* 1 = full size (default)
|
|
412
|
-
* 2 = 1/2 size
|
|
413
|
-
* 4 = 1/4 size
|
|
414
|
-
* 8 = 1/8 size
|
|
415
|
-
*/
|
|
416
827
|
setScale(scaleDenom) {
|
|
417
|
-
if (!this.decoder) {
|
|
418
|
-
throw new Error("Decoder not initialized");
|
|
419
|
-
}
|
|
420
|
-
if (this.started) {
|
|
421
|
-
throw new Error("Cannot change scale after decoding started");
|
|
422
|
-
}
|
|
423
828
|
if (this.module._sip_decoder_set_scale(this.decoder, scaleDenom) !== 0) {
|
|
424
829
|
throw new Error(`Invalid scale denominator: ${scaleDenom}`);
|
|
425
830
|
}
|
|
426
|
-
this.scaleDenom = scaleDenom;
|
|
427
831
|
this.outputWidth = this.module._sip_decoder_get_output_width(this.decoder);
|
|
428
832
|
this.outputHeight = this.module._sip_decoder_get_output_height(this.decoder);
|
|
429
833
|
return { width: this.outputWidth, height: this.outputHeight };
|
|
430
834
|
}
|
|
431
|
-
/**
|
|
432
|
-
* Get output dimensions (after any scaling)
|
|
433
|
-
*/
|
|
434
835
|
getOutputDimensions() {
|
|
435
836
|
return { width: this.outputWidth, height: this.outputHeight };
|
|
436
837
|
}
|
|
437
|
-
/**
|
|
438
|
-
* Start decoding
|
|
439
|
-
*/
|
|
440
838
|
start() {
|
|
441
|
-
|
|
442
|
-
|
|
839
|
+
const step = this.startStep();
|
|
840
|
+
if (step !== "ready") {
|
|
841
|
+
throw new Error("JPEG decoder needs more input before starting");
|
|
443
842
|
}
|
|
843
|
+
}
|
|
844
|
+
startStep() {
|
|
444
845
|
if (this.started) {
|
|
445
|
-
|
|
846
|
+
return "ready";
|
|
847
|
+
}
|
|
848
|
+
const result = this.module._sip_decoder_start(this.decoder);
|
|
849
|
+
if (result === 1) {
|
|
850
|
+
return "needMore";
|
|
446
851
|
}
|
|
447
|
-
if (
|
|
448
|
-
throw new Error("Failed to start decompression");
|
|
852
|
+
if (result !== 0) {
|
|
853
|
+
throw new Error("Failed to start JPEG decompression");
|
|
449
854
|
}
|
|
450
855
|
this.rowBufferPtr = this.module._sip_decoder_get_row_buffer(this.decoder);
|
|
451
856
|
if (!this.rowBufferPtr) {
|
|
452
|
-
throw new Error("Failed to get row buffer");
|
|
857
|
+
throw new Error("Failed to get JPEG decoder row buffer");
|
|
453
858
|
}
|
|
454
859
|
this.started = true;
|
|
860
|
+
return "ready";
|
|
455
861
|
}
|
|
456
|
-
/**
|
|
457
|
-
* Read next scanline
|
|
458
|
-
*
|
|
459
|
-
* @returns Scanline object or null if no more scanlines
|
|
460
|
-
*/
|
|
461
862
|
readScanline() {
|
|
863
|
+
const result = this.readScanlineStep();
|
|
864
|
+
if (result === "needMore") {
|
|
865
|
+
throw new Error("JPEG decoder needs more input");
|
|
866
|
+
}
|
|
867
|
+
return result;
|
|
868
|
+
}
|
|
869
|
+
readScanlineStep() {
|
|
462
870
|
if (!this.started || this.finished) {
|
|
463
871
|
return null;
|
|
464
872
|
}
|
|
465
873
|
const result = this.module._sip_decoder_read_scanline(this.decoder);
|
|
874
|
+
if (result === 2) {
|
|
875
|
+
return "needMore";
|
|
876
|
+
}
|
|
466
877
|
if (result === 0) {
|
|
467
878
|
this.finished = true;
|
|
468
879
|
return null;
|
|
469
880
|
}
|
|
470
|
-
if (result
|
|
471
|
-
throw new Error("Failed to read scanline");
|
|
881
|
+
if (result !== 1) {
|
|
882
|
+
throw new Error("Failed to read JPEG scanline");
|
|
472
883
|
}
|
|
473
|
-
const y = this.module._sip_decoder_get_scanline(this.decoder) - 1;
|
|
474
884
|
const rowSize = this.outputWidth * 3;
|
|
475
|
-
const data = new Uint8Array(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
rowSize
|
|
479
|
-
).slice();
|
|
480
|
-
return {
|
|
481
|
-
data,
|
|
482
|
-
width: this.outputWidth,
|
|
483
|
-
y
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Read all remaining scanlines
|
|
488
|
-
*
|
|
489
|
-
* @yields Scanline objects
|
|
490
|
-
*/
|
|
491
|
-
*readAllScanlines() {
|
|
492
|
-
let scanline;
|
|
493
|
-
while ((scanline = this.readScanline()) !== null) {
|
|
494
|
-
yield scanline;
|
|
495
|
-
}
|
|
885
|
+
const data = new Uint8Array(this.module.HEAPU8.buffer, this.rowBufferPtr, rowSize).slice();
|
|
886
|
+
const y = this.module._sip_decoder_get_scanline(this.decoder) - 1;
|
|
887
|
+
return { data, width: this.outputWidth, y };
|
|
496
888
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
*/
|
|
502
|
-
decodeAll() {
|
|
503
|
-
if (!this.started) {
|
|
504
|
-
this.start();
|
|
889
|
+
finishStep() {
|
|
890
|
+
const result = this.module._sip_decoder_finish(this.decoder);
|
|
891
|
+
if (result === 1) {
|
|
892
|
+
return "needMore";
|
|
505
893
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
for (const scanline of this.readAllScanlines()) {
|
|
509
|
-
pixels.set(scanline.data, scanline.y * rowSize);
|
|
894
|
+
if (result !== 0) {
|
|
895
|
+
throw new Error("Failed to finish JPEG decompression");
|
|
510
896
|
}
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
897
|
+
return "ready";
|
|
898
|
+
}
|
|
899
|
+
getBufferedInputSize() {
|
|
900
|
+
return this.module._sip_decoder_get_buffered_input_size(this.decoder);
|
|
901
|
+
}
|
|
902
|
+
getRowBufferSize() {
|
|
903
|
+
return this.module._sip_decoder_get_working_size(this.decoder);
|
|
516
904
|
}
|
|
517
|
-
/**
|
|
518
|
-
* Clean up resources
|
|
519
|
-
*/
|
|
520
905
|
dispose() {
|
|
521
906
|
if (this.decoder) {
|
|
522
907
|
this.module._sip_decoder_destroy(this.decoder);
|
|
523
908
|
this.decoder = 0;
|
|
524
909
|
}
|
|
525
|
-
|
|
526
|
-
this.module._free(this.dataPtr);
|
|
527
|
-
this.dataPtr = 0;
|
|
528
|
-
}
|
|
910
|
+
this.rowBufferPtr = 0;
|
|
529
911
|
this.started = false;
|
|
530
912
|
this.finished = false;
|
|
531
|
-
this.rowBufferPtr = 0;
|
|
532
913
|
}
|
|
533
914
|
};
|
|
534
915
|
function calculateOptimalScale(srcWidth, srcHeight, targetWidth, targetHeight) {
|
|
@@ -549,143 +930,125 @@ var WasmJpegEncoder = class {
|
|
|
549
930
|
encoder = 0;
|
|
550
931
|
width = 0;
|
|
551
932
|
height = 0;
|
|
552
|
-
quality = 85;
|
|
553
933
|
rowBufferPtr = 0;
|
|
554
934
|
started = false;
|
|
555
935
|
finished = false;
|
|
556
936
|
currentLine = 0;
|
|
557
937
|
constructor() {
|
|
558
938
|
this.module = getWasmModule();
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Initialize encoder with output dimensions and quality
|
|
562
|
-
*
|
|
563
|
-
* @param width - Output image width
|
|
564
|
-
* @param height - Output image height
|
|
565
|
-
* @param quality - JPEG quality (1-100, default 85)
|
|
566
|
-
*/
|
|
567
|
-
init(width, height, quality = 85) {
|
|
568
|
-
this.width = width;
|
|
569
|
-
this.height = height;
|
|
570
|
-
this.quality = Math.max(1, Math.min(100, quality));
|
|
571
939
|
this.encoder = this.module._sip_encoder_create();
|
|
572
940
|
if (!this.encoder) {
|
|
573
941
|
throw new Error("Failed to create JPEG encoder");
|
|
574
942
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
943
|
+
}
|
|
944
|
+
init(width, height, quality = 85) {
|
|
945
|
+
this.width = width;
|
|
946
|
+
this.height = height;
|
|
947
|
+
if (this.module._sip_encoder_init(this.encoder, width, height, quality) !== 0) {
|
|
948
|
+
throw new Error("Failed to initialize JPEG encoder");
|
|
578
949
|
}
|
|
579
950
|
}
|
|
580
|
-
/**
|
|
581
|
-
* Start encoding
|
|
582
|
-
*/
|
|
583
951
|
start() {
|
|
584
|
-
if (!this.encoder) {
|
|
585
|
-
throw new Error("Encoder not initialized");
|
|
586
|
-
}
|
|
587
952
|
if (this.started) {
|
|
588
|
-
|
|
953
|
+
return;
|
|
589
954
|
}
|
|
590
955
|
if (this.module._sip_encoder_start(this.encoder) !== 0) {
|
|
591
|
-
throw new Error("Failed to start compression");
|
|
956
|
+
throw new Error("Failed to start JPEG compression");
|
|
592
957
|
}
|
|
593
958
|
this.rowBufferPtr = this.module._sip_encoder_get_row_buffer(this.encoder);
|
|
594
959
|
if (!this.rowBufferPtr) {
|
|
595
|
-
throw new Error("Failed to get row buffer");
|
|
960
|
+
throw new Error("Failed to get JPEG encoder row buffer");
|
|
596
961
|
}
|
|
597
962
|
this.started = true;
|
|
598
963
|
this.currentLine = 0;
|
|
599
964
|
}
|
|
600
|
-
/**
|
|
601
|
-
* Write a scanline to the encoder
|
|
602
|
-
*
|
|
603
|
-
* @param scanline - Scanline with RGB data
|
|
604
|
-
*/
|
|
605
965
|
writeScanline(scanline) {
|
|
606
966
|
this.writeScanlineData(scanline.data);
|
|
607
967
|
}
|
|
608
|
-
/**
|
|
609
|
-
* Write raw RGB data as a scanline
|
|
610
|
-
*
|
|
611
|
-
* @param data - RGB data (width * 3 bytes)
|
|
612
|
-
*/
|
|
613
968
|
writeScanlineData(data) {
|
|
614
969
|
if (!this.started || this.finished) {
|
|
615
|
-
throw new Error("Encoder not ready for
|
|
616
|
-
}
|
|
617
|
-
if (this.currentLine >= this.height) {
|
|
618
|
-
throw new Error("All scanlines already written");
|
|
970
|
+
throw new Error("Encoder is not ready for scanlines");
|
|
619
971
|
}
|
|
620
972
|
const expectedSize = this.width * 3;
|
|
621
|
-
if (data.
|
|
622
|
-
throw new Error(`Invalid scanline size: expected ${expectedSize}, got ${data.
|
|
973
|
+
if (data.byteLength !== expectedSize) {
|
|
974
|
+
throw new Error(`Invalid scanline size: expected ${expectedSize}, got ${data.byteLength}`);
|
|
623
975
|
}
|
|
624
976
|
this.module.HEAPU8.set(data, this.rowBufferPtr);
|
|
625
977
|
if (this.module._sip_encoder_write_scanline(this.encoder) !== 1) {
|
|
626
|
-
throw new Error("Failed to write scanline");
|
|
978
|
+
throw new Error("Failed to write JPEG scanline");
|
|
627
979
|
}
|
|
628
980
|
this.currentLine++;
|
|
629
981
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
982
|
+
drainChunks() {
|
|
983
|
+
const chunks = [];
|
|
984
|
+
while (true) {
|
|
985
|
+
const ptr = this.module._sip_encoder_peek_chunk_data(this.encoder);
|
|
986
|
+
const size = this.module._sip_encoder_peek_chunk_size(this.encoder);
|
|
987
|
+
if (!ptr || !size) {
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
chunks.push(copyFromWasm(this.module, ptr, size));
|
|
991
|
+
this.module._sip_encoder_pop_chunk(this.encoder);
|
|
992
|
+
}
|
|
993
|
+
return chunks;
|
|
635
994
|
}
|
|
636
|
-
/**
|
|
637
|
-
* Finish encoding and get output
|
|
638
|
-
*
|
|
639
|
-
* @returns JPEG data as ArrayBuffer
|
|
640
|
-
*/
|
|
641
995
|
finish() {
|
|
642
996
|
if (!this.started) {
|
|
643
997
|
throw new Error("Encoding not started");
|
|
644
998
|
}
|
|
999
|
+
if (this.finished) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
645
1002
|
if (this.currentLine !== this.height) {
|
|
646
1003
|
throw new Error(`Incomplete image: wrote ${this.currentLine}/${this.height} scanlines`);
|
|
647
1004
|
}
|
|
648
1005
|
if (this.module._sip_encoder_finish(this.encoder) !== 0) {
|
|
649
|
-
throw new Error("Failed to finish
|
|
1006
|
+
throw new Error("Failed to finish JPEG compression");
|
|
650
1007
|
}
|
|
651
1008
|
this.finished = true;
|
|
652
|
-
|
|
653
|
-
const outputSize = this.module._sip_encoder_get_output_size(this.encoder);
|
|
654
|
-
if (!outputPtr || !outputSize) {
|
|
655
|
-
throw new Error("No output data");
|
|
656
|
-
}
|
|
657
|
-
const output = copyFromWasm(this.module, outputPtr, outputSize);
|
|
658
|
-
return output.buffer;
|
|
1009
|
+
return this.drainChunks();
|
|
659
1010
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Encode a full RGB buffer to JPEG
|
|
662
|
-
*
|
|
663
|
-
* @param pixels - RGB pixel data (width * height * 3 bytes)
|
|
664
|
-
* @returns JPEG data as ArrayBuffer
|
|
665
|
-
*/
|
|
666
1011
|
encodeAll(pixels) {
|
|
667
|
-
if (pixels.length !== this.width * this.height * 3) {
|
|
668
|
-
throw new Error(`Invalid pixel data size: expected ${this.width * this.height * 3}, got ${pixels.length}`);
|
|
669
|
-
}
|
|
670
1012
|
this.start();
|
|
671
1013
|
const rowSize = this.width * 3;
|
|
1014
|
+
const chunks = [];
|
|
1015
|
+
let total = 0;
|
|
672
1016
|
for (let y = 0; y < this.height; y++) {
|
|
673
|
-
|
|
674
|
-
this.
|
|
1017
|
+
this.writeScanlineData(pixels.subarray(y * rowSize, (y + 1) * rowSize));
|
|
1018
|
+
for (const chunk of this.drainChunks()) {
|
|
1019
|
+
chunks.push(chunk);
|
|
1020
|
+
total += chunk.byteLength;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
for (const chunk of this.finish()) {
|
|
1024
|
+
chunks.push(chunk);
|
|
1025
|
+
total += chunk.byteLength;
|
|
675
1026
|
}
|
|
676
|
-
|
|
1027
|
+
const merged = new Uint8Array(total);
|
|
1028
|
+
let offset = 0;
|
|
1029
|
+
for (const chunk of chunks) {
|
|
1030
|
+
merged.set(chunk, offset);
|
|
1031
|
+
offset += chunk.byteLength;
|
|
1032
|
+
}
|
|
1033
|
+
return merged.buffer.slice(merged.byteOffset, merged.byteOffset + merged.byteLength);
|
|
1034
|
+
}
|
|
1035
|
+
getBufferedOutputSize() {
|
|
1036
|
+
return this.module._sip_encoder_get_buffered_output_size(this.encoder);
|
|
1037
|
+
}
|
|
1038
|
+
getRowBufferSize() {
|
|
1039
|
+
return this.width * 3;
|
|
1040
|
+
}
|
|
1041
|
+
getCurrentLine() {
|
|
1042
|
+
return this.currentLine;
|
|
677
1043
|
}
|
|
678
|
-
/**
|
|
679
|
-
* Clean up resources
|
|
680
|
-
*/
|
|
681
1044
|
dispose() {
|
|
682
1045
|
if (this.encoder) {
|
|
683
1046
|
this.module._sip_encoder_destroy(this.encoder);
|
|
684
1047
|
this.encoder = 0;
|
|
685
1048
|
}
|
|
1049
|
+
this.rowBufferPtr = 0;
|
|
686
1050
|
this.started = false;
|
|
687
1051
|
this.finished = false;
|
|
688
|
-
this.rowBufferPtr = 0;
|
|
689
1052
|
this.currentLine = 0;
|
|
690
1053
|
}
|
|
691
1054
|
};
|
|
@@ -844,411 +1207,785 @@ var WasmPngDecoder = class {
|
|
|
844
1207
|
}
|
|
845
1208
|
};
|
|
846
1209
|
|
|
847
|
-
// src/
|
|
848
|
-
var
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
this.quality = quality;
|
|
858
|
-
await loadWasm();
|
|
859
|
-
this.wasmEncoder = new WasmJpegEncoder();
|
|
860
|
-
this.wasmEncoder.init(width, height, quality);
|
|
861
|
-
}
|
|
862
|
-
async encode(pixels) {
|
|
863
|
-
if (!this.wasmEncoder) {
|
|
864
|
-
throw new Error("Encoder not initialized. Call init() first.");
|
|
865
|
-
}
|
|
866
|
-
return this.wasmEncoder.encodeAll(pixels);
|
|
867
|
-
}
|
|
868
|
-
dispose() {
|
|
869
|
-
if (this.wasmEncoder) {
|
|
870
|
-
this.wasmEncoder.dispose();
|
|
871
|
-
this.wasmEncoder = null;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
};
|
|
875
|
-
async function createEncoder(width, height, quality) {
|
|
876
|
-
const encoder = new NativeEncoder();
|
|
877
|
-
await encoder.init(width, height, quality);
|
|
878
|
-
return encoder;
|
|
1210
|
+
// src/api.ts
|
|
1211
|
+
var DEFAULT_QUALITY = 85;
|
|
1212
|
+
function createDeferred() {
|
|
1213
|
+
let resolve;
|
|
1214
|
+
let reject;
|
|
1215
|
+
const promise = new Promise((res, rej) => {
|
|
1216
|
+
resolve = res;
|
|
1217
|
+
reject = rej;
|
|
1218
|
+
});
|
|
1219
|
+
return { resolve, reject, promise };
|
|
879
1220
|
}
|
|
880
|
-
|
|
881
|
-
// src/resize.ts
|
|
882
|
-
function createResizeState(srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
1221
|
+
function makeEmptyStats() {
|
|
883
1222
|
return {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
bufferBY: -1,
|
|
892
|
-
currentOutputY: 0
|
|
1223
|
+
peakPipelineBytes: 0,
|
|
1224
|
+
peakCodecBytes: 0,
|
|
1225
|
+
peakBufferedInputBytes: 0,
|
|
1226
|
+
peakBufferedOutputBytes: 0,
|
|
1227
|
+
bytesIn: 0,
|
|
1228
|
+
bytesOut: 0,
|
|
1229
|
+
notes: []
|
|
893
1230
|
};
|
|
894
1231
|
}
|
|
895
|
-
function
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
const srcXFloat = dstX * xScale;
|
|
900
|
-
const srcX0 = Math.floor(srcXFloat);
|
|
901
|
-
const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);
|
|
902
|
-
const t = srcXFloat - srcX0;
|
|
903
|
-
const invT = 1 - t;
|
|
904
|
-
const src0 = srcX0 * 3;
|
|
905
|
-
const src1 = srcX1 * 3;
|
|
906
|
-
const dstOffset = dstX * 3;
|
|
907
|
-
dst[dstOffset] = Math.round(src[src0] * invT + src[src1] * t);
|
|
908
|
-
dst[dstOffset + 1] = Math.round(src[src0 + 1] * invT + src[src1 + 1] * t);
|
|
909
|
-
dst[dstOffset + 2] = Math.round(src[src0 + 2] * invT + src[src1 + 2] * t);
|
|
1232
|
+
function concatUint8Arrays(chunks) {
|
|
1233
|
+
let total = 0;
|
|
1234
|
+
for (const chunk of chunks) {
|
|
1235
|
+
total += chunk.byteLength;
|
|
910
1236
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
for (let i = 0; i < width * 3; i++) {
|
|
917
|
-
result[i] = Math.round(rowA[i] * invT + rowB[i] * t);
|
|
1237
|
+
const merged = new Uint8Array(total);
|
|
1238
|
+
let offset = 0;
|
|
1239
|
+
for (const chunk of chunks) {
|
|
1240
|
+
merged.set(chunk, offset);
|
|
1241
|
+
offset += chunk.byteLength;
|
|
918
1242
|
}
|
|
919
|
-
return
|
|
1243
|
+
return merged;
|
|
920
1244
|
}
|
|
921
|
-
function
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (
|
|
1245
|
+
function readJpegOrientation(bytes) {
|
|
1246
|
+
if (bytes.byteLength < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
let offset = 2;
|
|
1250
|
+
while (offset + 4 <= bytes.byteLength) {
|
|
1251
|
+
if (bytes[offset] !== 255) {
|
|
1252
|
+
offset++;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
while (offset < bytes.byteLength && bytes[offset] === 255) {
|
|
1256
|
+
offset++;
|
|
1257
|
+
}
|
|
1258
|
+
if (offset >= bytes.byteLength) {
|
|
935
1259
|
break;
|
|
936
1260
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
data: state.bufferB,
|
|
940
|
-
width: dstWidth,
|
|
941
|
-
y: state.currentOutputY
|
|
942
|
-
});
|
|
943
|
-
state.currentOutputY++;
|
|
1261
|
+
const marker = bytes[offset++];
|
|
1262
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
944
1263
|
continue;
|
|
945
1264
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
let rowB = state.bufferB;
|
|
949
|
-
if (srcYFloor === state.bufferBY) {
|
|
950
|
-
rowA = state.bufferB;
|
|
951
|
-
rowB = state.bufferB;
|
|
952
|
-
} else if (srcYCeil === state.bufferAY) {
|
|
953
|
-
rowA = state.bufferA;
|
|
954
|
-
rowB = state.bufferA;
|
|
1265
|
+
if (marker === 217 || marker === 218) {
|
|
1266
|
+
break;
|
|
955
1267
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1268
|
+
if (offset + 2 > bytes.byteLength) {
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
const segmentLength = bytes[offset] << 8 | bytes[offset + 1];
|
|
1272
|
+
if (segmentLength < 2 || offset + segmentLength > bytes.byteLength) {
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
const segmentStart = offset + 2;
|
|
1276
|
+
const payloadLength = segmentLength - 2;
|
|
1277
|
+
if (marker === 225 && payloadLength >= 14 && bytes[segmentStart] === 69 && bytes[segmentStart + 1] === 120 && bytes[segmentStart + 2] === 105 && bytes[segmentStart + 3] === 102 && bytes[segmentStart + 4] === 0 && bytes[segmentStart + 5] === 0) {
|
|
1278
|
+
const tiff = segmentStart + 6;
|
|
1279
|
+
if (tiff + 8 > bytes.byteLength) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
const littleEndian = bytes[tiff] === 73 && bytes[tiff + 1] === 73;
|
|
1283
|
+
const bigEndian = bytes[tiff] === 77 && bytes[tiff + 1] === 77;
|
|
1284
|
+
if (!littleEndian && !bigEndian) {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
const read16 = (index) => littleEndian ? bytes[index] | bytes[index + 1] << 8 : bytes[index] << 8 | bytes[index + 1];
|
|
1288
|
+
const read32 = (index) => littleEndian ? (bytes[index] | bytes[index + 1] << 8 | bytes[index + 2] << 16 | bytes[index + 3] << 24) >>> 0 : (bytes[index] << 24 | bytes[index + 1] << 16 | bytes[index + 2] << 8 | bytes[index + 3]) >>> 0;
|
|
1289
|
+
const ifdOffset = read32(tiff + 4);
|
|
1290
|
+
const ifdStart = tiff + ifdOffset;
|
|
1291
|
+
if (ifdStart + 2 > bytes.byteLength) {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
const entryCount = read16(ifdStart);
|
|
1295
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1296
|
+
const entry = ifdStart + 2 + i * 12;
|
|
1297
|
+
if (entry + 12 > bytes.byteLength) {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
const tag = read16(entry);
|
|
1301
|
+
if (tag !== 274) {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
const type = read16(entry + 2);
|
|
1305
|
+
const count = read32(entry + 4);
|
|
1306
|
+
if (type !== 3 || count !== 1) {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
const valueOffset = entry + 8;
|
|
1310
|
+
return littleEndian ? bytes[valueOffset] | bytes[valueOffset + 1] << 8 : bytes[valueOffset] << 8 | bytes[valueOffset + 1];
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
offset += segmentLength;
|
|
963
1314
|
}
|
|
964
|
-
return
|
|
1315
|
+
return null;
|
|
965
1316
|
}
|
|
966
|
-
function
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1317
|
+
function buildExifOrientationSegment(orientation) {
|
|
1318
|
+
if (!Number.isInteger(orientation) || orientation < 2 || orientation > 8) {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
const payload = new Uint8Array([
|
|
1322
|
+
69,
|
|
1323
|
+
120,
|
|
1324
|
+
105,
|
|
1325
|
+
102,
|
|
1326
|
+
0,
|
|
1327
|
+
0,
|
|
1328
|
+
73,
|
|
1329
|
+
73,
|
|
1330
|
+
42,
|
|
1331
|
+
0,
|
|
1332
|
+
8,
|
|
1333
|
+
0,
|
|
1334
|
+
0,
|
|
1335
|
+
0,
|
|
1336
|
+
1,
|
|
1337
|
+
0,
|
|
1338
|
+
18,
|
|
1339
|
+
1,
|
|
1340
|
+
3,
|
|
1341
|
+
0,
|
|
1342
|
+
1,
|
|
1343
|
+
0,
|
|
1344
|
+
0,
|
|
1345
|
+
0,
|
|
1346
|
+
orientation & 255,
|
|
1347
|
+
0,
|
|
1348
|
+
0,
|
|
1349
|
+
0,
|
|
1350
|
+
0,
|
|
1351
|
+
0,
|
|
1352
|
+
0,
|
|
1353
|
+
0
|
|
1354
|
+
]);
|
|
1355
|
+
const length = payload.byteLength + 2;
|
|
1356
|
+
const segment = new Uint8Array(payload.byteLength + 4);
|
|
1357
|
+
segment[0] = 255;
|
|
1358
|
+
segment[1] = 225;
|
|
1359
|
+
segment[2] = length >> 8 & 255;
|
|
1360
|
+
segment[3] = length & 255;
|
|
1361
|
+
segment.set(payload, 4);
|
|
1362
|
+
return segment;
|
|
1363
|
+
}
|
|
1364
|
+
function injectJpegApp1Segment(chunk, segment) {
|
|
1365
|
+
if (chunk.byteLength < 2 || chunk[0] !== 255 || chunk[1] !== 216) {
|
|
1366
|
+
return concatUint8Arrays([chunk, segment]);
|
|
1367
|
+
}
|
|
1368
|
+
const merged = new Uint8Array(chunk.byteLength + segment.byteLength);
|
|
1369
|
+
merged[0] = 255;
|
|
1370
|
+
merged[1] = 216;
|
|
1371
|
+
merged.set(segment, 2);
|
|
1372
|
+
merged.set(chunk.subarray(2), 2 + segment.byteLength);
|
|
1373
|
+
return merged;
|
|
1374
|
+
}
|
|
1375
|
+
async function readJpegOrientationFromSource(source) {
|
|
1376
|
+
const direct = readJpegOrientation(source.headerBytes);
|
|
1377
|
+
if (direct !== null) {
|
|
1378
|
+
return direct;
|
|
976
1379
|
}
|
|
977
|
-
|
|
1380
|
+
const extended = await source.ensureHeaderBytes(262144);
|
|
1381
|
+
return readJpegOrientation(extended);
|
|
978
1382
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1383
|
+
var StatsTracker = class {
|
|
1384
|
+
stats = makeEmptyStats();
|
|
1385
|
+
constructor(note) {
|
|
1386
|
+
if (note) {
|
|
1387
|
+
this.note(note);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
note(message) {
|
|
1391
|
+
if (!this.stats.notes.includes(message)) {
|
|
1392
|
+
this.stats.notes.push(message);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
addBytesIn(bytes) {
|
|
1396
|
+
this.stats.bytesIn += bytes;
|
|
1397
|
+
}
|
|
1398
|
+
addBytesOut(bytes) {
|
|
1399
|
+
this.stats.bytesOut += bytes;
|
|
1400
|
+
}
|
|
1401
|
+
update(bufferedInput, bufferedOutput, codecBytes, pipelineBytes) {
|
|
1402
|
+
this.stats.peakBufferedInputBytes = Math.max(this.stats.peakBufferedInputBytes, bufferedInput);
|
|
1403
|
+
this.stats.peakBufferedOutputBytes = Math.max(this.stats.peakBufferedOutputBytes, bufferedOutput);
|
|
1404
|
+
this.stats.peakCodecBytes = Math.max(this.stats.peakCodecBytes, codecBytes);
|
|
1405
|
+
this.stats.peakPipelineBytes = Math.max(this.stats.peakPipelineBytes, pipelineBytes);
|
|
1406
|
+
}
|
|
1407
|
+
snapshot() {
|
|
1408
|
+
return { ...this.stats, notes: [...this.stats.notes] };
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
function normalizeBox(options, width, height) {
|
|
1412
|
+
return calculateTargetDimensions(
|
|
1413
|
+
width,
|
|
1414
|
+
height,
|
|
1415
|
+
options.width ?? width,
|
|
1416
|
+
options.height ?? height
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
function createPixelStream(iteratorFactory, info, stats = Promise.resolve(makeEmptyStats())) {
|
|
983
1420
|
return {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1421
|
+
info,
|
|
1422
|
+
stats,
|
|
1423
|
+
[Symbol.asyncIterator]() {
|
|
1424
|
+
return iteratorFactory()[Symbol.asyncIterator]();
|
|
1425
|
+
}
|
|
987
1426
|
};
|
|
988
1427
|
}
|
|
989
|
-
function
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
return scale;
|
|
1428
|
+
function createEncodedImage(iteratorFactory, info, stats) {
|
|
1429
|
+
return {
|
|
1430
|
+
info,
|
|
1431
|
+
stats,
|
|
1432
|
+
[Symbol.asyncIterator]() {
|
|
1433
|
+
return iteratorFactory()[Symbol.asyncIterator]();
|
|
996
1434
|
}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
async function* iterateUint8ArrayRows(pixels, width, height) {
|
|
1438
|
+
const rowSize = width * 3;
|
|
1439
|
+
for (let y = 0; y < height; y++) {
|
|
1440
|
+
yield {
|
|
1441
|
+
data: pixels.subarray(y * rowSize, (y + 1) * rowSize),
|
|
1442
|
+
width,
|
|
1443
|
+
y
|
|
1444
|
+
};
|
|
997
1445
|
}
|
|
998
|
-
return 1;
|
|
999
1446
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1447
|
+
async function* iterateInputChunks(source) {
|
|
1448
|
+
const iterator = source.open()[Symbol.asyncIterator]();
|
|
1449
|
+
let current = await iterator.next();
|
|
1450
|
+
if (current.done) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
while (true) {
|
|
1454
|
+
const next = await iterator.next();
|
|
1455
|
+
yield {
|
|
1456
|
+
chunk: current.value,
|
|
1457
|
+
isFinal: next.done === true
|
|
1458
|
+
};
|
|
1459
|
+
if (next.done === true) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
current = next;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
async function* decodeSourceInternal(input) {
|
|
1466
|
+
const prepared = await prepareInputSource(input);
|
|
1467
|
+
const info = await inspectSource(prepared);
|
|
1468
|
+
if (info.format === "unknown") {
|
|
1469
|
+
throw new Error("Unsupported image format");
|
|
1470
|
+
}
|
|
1010
1471
|
await loadWasm();
|
|
1011
|
-
|
|
1472
|
+
if (info.format === "jpeg") {
|
|
1473
|
+
const decoder2 = new WasmJpegDecoder();
|
|
1474
|
+
try {
|
|
1475
|
+
if (prepared.kind === "bytes") {
|
|
1476
|
+
const bytes2 = await collectSourceBytes(prepared);
|
|
1477
|
+
decoder2.init(asArrayBuffer(bytes2));
|
|
1478
|
+
decoder2.start();
|
|
1479
|
+
while (true) {
|
|
1480
|
+
const scanline = decoder2.readScanline();
|
|
1481
|
+
if (!scanline) {
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
yield scanline;
|
|
1485
|
+
}
|
|
1486
|
+
if (decoder2.finishStep() !== "ready") {
|
|
1487
|
+
throw new Error("Unexpected end of JPEG input while finishing");
|
|
1488
|
+
}
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
let headerReady = false;
|
|
1492
|
+
let started = false;
|
|
1493
|
+
for await (const { chunk, isFinal } of iterateInputChunks(prepared)) {
|
|
1494
|
+
decoder2.pushInput(chunk, isFinal);
|
|
1495
|
+
if (!headerReady) {
|
|
1496
|
+
const headerStep = decoder2.readHeaderStep();
|
|
1497
|
+
if (headerStep === "ready") {
|
|
1498
|
+
headerReady = true;
|
|
1499
|
+
} else {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (!started) {
|
|
1504
|
+
const startStep = decoder2.startStep();
|
|
1505
|
+
if (startStep === "ready") {
|
|
1506
|
+
started = true;
|
|
1507
|
+
} else {
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
while (true) {
|
|
1512
|
+
const scanline = decoder2.readScanlineStep();
|
|
1513
|
+
if (scanline === "needMore") {
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
if (scanline === null) {
|
|
1517
|
+
if (decoder2.finishStep() !== "ready") {
|
|
1518
|
+
throw new Error("Unexpected end of JPEG input while finishing");
|
|
1519
|
+
}
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
yield scanline;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (!headerReady) {
|
|
1526
|
+
if (decoder2.readHeaderStep() !== "ready") {
|
|
1527
|
+
throw new Error("Incomplete JPEG image");
|
|
1528
|
+
}
|
|
1529
|
+
headerReady = true;
|
|
1530
|
+
}
|
|
1531
|
+
if (!started) {
|
|
1532
|
+
if (decoder2.startStep() !== "ready") {
|
|
1533
|
+
throw new Error("Incomplete JPEG image");
|
|
1534
|
+
}
|
|
1535
|
+
started = true;
|
|
1536
|
+
}
|
|
1537
|
+
while (true) {
|
|
1538
|
+
const scanline = decoder2.readScanlineStep();
|
|
1539
|
+
if (scanline === "needMore") {
|
|
1540
|
+
throw new Error("Unexpected end of JPEG input");
|
|
1541
|
+
}
|
|
1542
|
+
if (scanline === null) {
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
yield scanline;
|
|
1546
|
+
}
|
|
1547
|
+
if (decoder2.finishStep() !== "ready") {
|
|
1548
|
+
throw new Error("Unexpected end of JPEG input while finishing");
|
|
1549
|
+
}
|
|
1550
|
+
return;
|
|
1551
|
+
} finally {
|
|
1552
|
+
decoder2.dispose();
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const bytes = await collectSourceBytes(prepared);
|
|
1556
|
+
const buffer = asArrayBuffer(bytes);
|
|
1557
|
+
if (info.format === "png") {
|
|
1558
|
+
const decoder2 = new WasmPngDecoder();
|
|
1559
|
+
try {
|
|
1560
|
+
decoder2.init(buffer);
|
|
1561
|
+
decoder2.start();
|
|
1562
|
+
for (const scanline of decoder2.readAllScanlines()) {
|
|
1563
|
+
yield scanline;
|
|
1564
|
+
}
|
|
1565
|
+
} finally {
|
|
1566
|
+
decoder2.dispose();
|
|
1567
|
+
}
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const decoder = await createDecoder(info.format, buffer);
|
|
1012
1571
|
try {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1572
|
+
const decoded = await decoder.decode();
|
|
1573
|
+
yield* iterateUint8ArrayRows(decoded.pixels, decoded.width, decoded.height);
|
|
1574
|
+
} finally {
|
|
1575
|
+
decoder.dispose();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function decode(input) {
|
|
1579
|
+
const infoDeferred = createDeferred();
|
|
1580
|
+
const iteratorFactory = () => (async function* decodeIterator() {
|
|
1581
|
+
const prepared = await prepareInputSource(input);
|
|
1582
|
+
const info = await inspectSource(prepared);
|
|
1583
|
+
if (info.format === "unknown") {
|
|
1584
|
+
throw new Error("Unsupported image format");
|
|
1585
|
+
}
|
|
1586
|
+
infoDeferred.resolve({
|
|
1587
|
+
width: info.width,
|
|
1588
|
+
height: info.height,
|
|
1589
|
+
originalFormat: info.format
|
|
1590
|
+
});
|
|
1591
|
+
yield* decodeSourceInternal(prepared);
|
|
1592
|
+
})();
|
|
1593
|
+
return createPixelStream(iteratorFactory, infoDeferred.promise);
|
|
1594
|
+
}
|
|
1595
|
+
function resize(stream, options) {
|
|
1596
|
+
const infoPromise = stream.info.then((info) => {
|
|
1597
|
+
const target = normalizeBox(options, info.width, info.height);
|
|
1598
|
+
return {
|
|
1599
|
+
width: target.width,
|
|
1600
|
+
height: target.height,
|
|
1601
|
+
originalFormat: info.originalFormat
|
|
1602
|
+
};
|
|
1603
|
+
});
|
|
1604
|
+
const iteratorFactory = () => (async function* resizeIterator() {
|
|
1605
|
+
const sourceInfo = await stream.info;
|
|
1606
|
+
const target = normalizeBox(options, sourceInfo.width, sourceInfo.height);
|
|
1607
|
+
const state = createResizeState(
|
|
1608
|
+
sourceInfo.width,
|
|
1609
|
+
sourceInfo.height,
|
|
1030
1610
|
target.width,
|
|
1031
1611
|
target.height
|
|
1032
1612
|
);
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
let decodedLine = 0;
|
|
1038
|
-
for (const scanline of decoder.readAllScanlines()) {
|
|
1039
|
-
const outputScanlines = processScanline(resizeState, scanline.data, decodedLine);
|
|
1040
|
-
decodedLine++;
|
|
1041
|
-
for (const outScanline of outputScanlines) {
|
|
1042
|
-
encoder.writeScanline(outScanline);
|
|
1613
|
+
for await (const scanline of stream) {
|
|
1614
|
+
const output = processScanline(state, scanline.data, scanline.y);
|
|
1615
|
+
for (const next of output) {
|
|
1616
|
+
yield next;
|
|
1043
1617
|
}
|
|
1044
1618
|
}
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
encoder.writeScanline(outScanline);
|
|
1619
|
+
for (const next of flushResize(state)) {
|
|
1620
|
+
yield next;
|
|
1048
1621
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1622
|
+
})();
|
|
1623
|
+
return createPixelStream(iteratorFactory, infoPromise, stream.stats ?? Promise.resolve(makeEmptyStats()));
|
|
1624
|
+
}
|
|
1625
|
+
function encodeJpeg(stream, options = {}) {
|
|
1626
|
+
const quality = options.quality ?? DEFAULT_QUALITY;
|
|
1627
|
+
const infoPromise = stream.info.then((info) => ({
|
|
1628
|
+
width: info.width,
|
|
1629
|
+
height: info.height,
|
|
1630
|
+
mimeType: "image/jpeg",
|
|
1631
|
+
originalFormat: info.originalFormat
|
|
1632
|
+
}));
|
|
1633
|
+
const statsPromise = stream.stats ?? Promise.resolve(makeEmptyStats());
|
|
1634
|
+
const iteratorFactory = () => (async function* encodeIterator() {
|
|
1635
|
+
await loadWasm();
|
|
1636
|
+
const info = await stream.info;
|
|
1637
|
+
const encoder = new WasmJpegEncoder();
|
|
1638
|
+
try {
|
|
1639
|
+
encoder.init(info.width, info.height, quality);
|
|
1640
|
+
encoder.start();
|
|
1641
|
+
for await (const scanline of stream) {
|
|
1642
|
+
encoder.writeScanline(scanline);
|
|
1643
|
+
for (const chunk of encoder.drainChunks()) {
|
|
1644
|
+
yield chunk;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
for (const chunk of encoder.finish()) {
|
|
1648
|
+
yield chunk;
|
|
1649
|
+
}
|
|
1650
|
+
} finally {
|
|
1051
1651
|
encoder.dispose();
|
|
1052
|
-
decoder.dispose();
|
|
1053
|
-
return processJpegStreaming(input, {
|
|
1054
|
-
...opts,
|
|
1055
|
-
quality: opts.quality - 10
|
|
1056
|
-
});
|
|
1057
1652
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
data: jpegData,
|
|
1061
|
-
width: target.width,
|
|
1062
|
-
height: target.height,
|
|
1063
|
-
mimeType: "image/jpeg",
|
|
1064
|
-
originalFormat: "jpeg"
|
|
1065
|
-
};
|
|
1066
|
-
} finally {
|
|
1067
|
-
decoder.dispose();
|
|
1068
|
-
}
|
|
1653
|
+
})();
|
|
1654
|
+
return createEncodedImage(iteratorFactory, infoPromise, statsPromise);
|
|
1069
1655
|
}
|
|
1070
|
-
async function
|
|
1071
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1656
|
+
async function* runJpegTransform(source, info, options, infoDeferred, stats) {
|
|
1072
1657
|
await loadWasm();
|
|
1073
|
-
const
|
|
1658
|
+
const orientation = await readJpegOrientationFromSource(source);
|
|
1659
|
+
const orientationSegment = orientation ? buildExifOrientationSegment(orientation) : null;
|
|
1660
|
+
const target = normalizeBox(options, info.width, info.height);
|
|
1661
|
+
const decoder = new WasmJpegDecoder();
|
|
1662
|
+
const encoder = new WasmJpegEncoder();
|
|
1663
|
+
let resizeState = createResizeState(1, 1, target.width, target.height);
|
|
1664
|
+
let decodeWidth = info.width;
|
|
1665
|
+
let decodeHeight = info.height;
|
|
1666
|
+
const scale = calculateOptimalScale(info.width, info.height, target.width, target.height);
|
|
1667
|
+
let headerReady = false;
|
|
1668
|
+
let started = false;
|
|
1669
|
+
let emittedFirstChunk = false;
|
|
1670
|
+
const refresh = () => {
|
|
1671
|
+
const resizeBytes = (resizeState.bufferA?.byteLength ?? 0) + (resizeState.bufferB?.byteLength ?? 0);
|
|
1672
|
+
const codecBytes = decoder.getBufferedInputSize() + decoder.getRowBufferSize() + encoder.getBufferedOutputSize() + encoder.getRowBufferSize();
|
|
1673
|
+
const pipelineBytes = codecBytes + resizeBytes;
|
|
1674
|
+
stats.update(decoder.getBufferedInputSize(), encoder.getBufferedOutputSize(), codecBytes, pipelineBytes);
|
|
1675
|
+
};
|
|
1074
1676
|
try {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
target.height
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1677
|
+
if (source.kind === "bytes") {
|
|
1678
|
+
const bytes = await collectSourceBytes(source);
|
|
1679
|
+
stats.addBytesIn(bytes.byteLength);
|
|
1680
|
+
refresh();
|
|
1681
|
+
if (orientationSegment) {
|
|
1682
|
+
stats.note(`jpeg-orientation=${orientation}`);
|
|
1683
|
+
}
|
|
1684
|
+
decoder.init(asArrayBuffer(bytes));
|
|
1685
|
+
const output = decoder.setScale(scale);
|
|
1686
|
+
decodeWidth = output.width;
|
|
1687
|
+
decodeHeight = output.height;
|
|
1688
|
+
resizeState = createResizeState(output.width, output.height, target.width, target.height);
|
|
1689
|
+
encoder.init(target.width, target.height, options.quality ?? DEFAULT_QUALITY);
|
|
1690
|
+
encoder.start();
|
|
1691
|
+
decoder.start();
|
|
1692
|
+
headerReady = true;
|
|
1693
|
+
started = true;
|
|
1694
|
+
infoDeferred.resolve({
|
|
1695
|
+
width: target.width,
|
|
1696
|
+
height: target.height,
|
|
1697
|
+
mimeType: "image/jpeg",
|
|
1698
|
+
originalFormat: "jpeg"
|
|
1699
|
+
});
|
|
1700
|
+
stats.note(`jpeg-dct-scale=1/${scale}`);
|
|
1701
|
+
stats.note(`jpeg-decoded=${decodeWidth}x${decodeHeight}`);
|
|
1702
|
+
refresh();
|
|
1703
|
+
while (true) {
|
|
1704
|
+
const scanline = decoder.readScanline();
|
|
1705
|
+
if (!scanline) {
|
|
1706
|
+
break;
|
|
1707
|
+
}
|
|
1708
|
+
const outputScanlines = processScanline(resizeState, scanline.data, scanline.y);
|
|
1709
|
+
refresh();
|
|
1710
|
+
for (const outScanline of outputScanlines) {
|
|
1711
|
+
encoder.writeScanline(outScanline);
|
|
1712
|
+
refresh();
|
|
1713
|
+
for (const jpegChunk of encoder.drainChunks()) {
|
|
1714
|
+
const nextChunk = !emittedFirstChunk && orientationSegment ? injectJpegApp1Segment(jpegChunk, orientationSegment) : jpegChunk;
|
|
1715
|
+
emittedFirstChunk = true;
|
|
1716
|
+
stats.addBytesOut(nextChunk.byteLength);
|
|
1717
|
+
refresh();
|
|
1718
|
+
yield nextChunk;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (decoder.finishStep() !== "ready") {
|
|
1723
|
+
throw new Error("Unexpected end of JPEG input while finishing");
|
|
1724
|
+
}
|
|
1725
|
+
for (const outScanline of flushResize(resizeState)) {
|
|
1097
1726
|
encoder.writeScanline(outScanline);
|
|
1727
|
+
refresh();
|
|
1728
|
+
for (const jpegChunk of encoder.drainChunks()) {
|
|
1729
|
+
const nextChunk = !emittedFirstChunk && orientationSegment ? injectJpegApp1Segment(jpegChunk, orientationSegment) : jpegChunk;
|
|
1730
|
+
emittedFirstChunk = true;
|
|
1731
|
+
stats.addBytesOut(nextChunk.byteLength);
|
|
1732
|
+
refresh();
|
|
1733
|
+
yield nextChunk;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
for (const jpegChunk of encoder.finish()) {
|
|
1737
|
+
const nextChunk = !emittedFirstChunk && orientationSegment ? injectJpegApp1Segment(jpegChunk, orientationSegment) : jpegChunk;
|
|
1738
|
+
emittedFirstChunk = true;
|
|
1739
|
+
stats.addBytesOut(nextChunk.byteLength);
|
|
1740
|
+
refresh();
|
|
1741
|
+
yield nextChunk;
|
|
1742
|
+
}
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (orientationSegment) {
|
|
1746
|
+
stats.note(`jpeg-orientation=${orientation}`);
|
|
1747
|
+
}
|
|
1748
|
+
for await (const { chunk, isFinal } of iterateInputChunks(source)) {
|
|
1749
|
+
stats.addBytesIn(chunk.byteLength);
|
|
1750
|
+
decoder.pushInput(chunk, isFinal);
|
|
1751
|
+
refresh();
|
|
1752
|
+
if (!headerReady) {
|
|
1753
|
+
const headerStep = decoder.readHeaderStep();
|
|
1754
|
+
if (headerStep === "needMore") {
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
headerReady = true;
|
|
1758
|
+
const output = decoder.setScale(scale);
|
|
1759
|
+
decodeWidth = output.width;
|
|
1760
|
+
decodeHeight = output.height;
|
|
1761
|
+
resizeState = createResizeState(output.width, output.height, target.width, target.height);
|
|
1762
|
+
encoder.init(target.width, target.height, options.quality ?? DEFAULT_QUALITY);
|
|
1763
|
+
encoder.start();
|
|
1764
|
+
infoDeferred.resolve({
|
|
1765
|
+
width: target.width,
|
|
1766
|
+
height: target.height,
|
|
1767
|
+
mimeType: "image/jpeg",
|
|
1768
|
+
originalFormat: "jpeg"
|
|
1769
|
+
});
|
|
1770
|
+
stats.note(`jpeg-dct-scale=1/${scale}`);
|
|
1771
|
+
stats.note(`jpeg-decoded=${decodeWidth}x${decodeHeight}`);
|
|
1772
|
+
refresh();
|
|
1773
|
+
}
|
|
1774
|
+
if (!started) {
|
|
1775
|
+
const startStep = decoder.startStep();
|
|
1776
|
+
if (startStep === "needMore") {
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
started = true;
|
|
1780
|
+
refresh();
|
|
1781
|
+
}
|
|
1782
|
+
while (true) {
|
|
1783
|
+
const scanline = decoder.readScanlineStep();
|
|
1784
|
+
if (scanline === "needMore") {
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
if (scanline === null) {
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
const outputScanlines = processScanline(resizeState, scanline.data, scanline.y);
|
|
1791
|
+
refresh();
|
|
1792
|
+
for (const outScanline of outputScanlines) {
|
|
1793
|
+
encoder.writeScanline(outScanline);
|
|
1794
|
+
refresh();
|
|
1795
|
+
for (const jpegChunk of encoder.drainChunks()) {
|
|
1796
|
+
const nextChunk = !emittedFirstChunk && orientationSegment ? injectJpegApp1Segment(jpegChunk, orientationSegment) : jpegChunk;
|
|
1797
|
+
emittedFirstChunk = true;
|
|
1798
|
+
stats.addBytesOut(nextChunk.byteLength);
|
|
1799
|
+
refresh();
|
|
1800
|
+
yield nextChunk;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1098
1803
|
}
|
|
1099
1804
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1805
|
+
if (decoder.finishStep() !== "ready") {
|
|
1806
|
+
throw new Error("Unexpected end of JPEG input while finishing");
|
|
1807
|
+
}
|
|
1808
|
+
for (const outScanline of flushResize(resizeState)) {
|
|
1102
1809
|
encoder.writeScanline(outScanline);
|
|
1810
|
+
refresh();
|
|
1811
|
+
for (const jpegChunk of encoder.drainChunks()) {
|
|
1812
|
+
const nextChunk = !emittedFirstChunk && orientationSegment ? injectJpegApp1Segment(jpegChunk, orientationSegment) : jpegChunk;
|
|
1813
|
+
emittedFirstChunk = true;
|
|
1814
|
+
stats.addBytesOut(nextChunk.byteLength);
|
|
1815
|
+
refresh();
|
|
1816
|
+
yield nextChunk;
|
|
1817
|
+
}
|
|
1103
1818
|
}
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
quality: opts.quality - 10
|
|
1111
|
-
});
|
|
1819
|
+
for (const jpegChunk of encoder.finish()) {
|
|
1820
|
+
const nextChunk = !emittedFirstChunk && orientationSegment ? injectJpegApp1Segment(jpegChunk, orientationSegment) : jpegChunk;
|
|
1821
|
+
emittedFirstChunk = true;
|
|
1822
|
+
stats.addBytesOut(nextChunk.byteLength);
|
|
1823
|
+
refresh();
|
|
1824
|
+
yield nextChunk;
|
|
1112
1825
|
}
|
|
1113
|
-
encoder.dispose();
|
|
1114
|
-
return {
|
|
1115
|
-
data: jpegData,
|
|
1116
|
-
width: target.width,
|
|
1117
|
-
height: target.height,
|
|
1118
|
-
mimeType: "image/jpeg",
|
|
1119
|
-
originalFormat: "png"
|
|
1120
|
-
};
|
|
1121
1826
|
} finally {
|
|
1122
1827
|
decoder.dispose();
|
|
1828
|
+
encoder.dispose();
|
|
1123
1829
|
}
|
|
1124
1830
|
}
|
|
1125
|
-
function
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1831
|
+
async function* runBufferedTransform(source, info, options, infoDeferred, stats) {
|
|
1832
|
+
const bytes = await collectSourceBytes(source);
|
|
1833
|
+
stats.addBytesIn(bytes.byteLength);
|
|
1834
|
+
stats.update(bytes.byteLength, 0, bytes.byteLength, bytes.byteLength);
|
|
1835
|
+
stats.note(`${info.format}-input-buffered`);
|
|
1836
|
+
await loadWasm();
|
|
1837
|
+
const target = normalizeBox(options, info.width, info.height);
|
|
1838
|
+
const encoder = new WasmJpegEncoder();
|
|
1839
|
+
let scanlines;
|
|
1840
|
+
if (info.format === "png") {
|
|
1841
|
+
const decoder = new WasmPngDecoder();
|
|
1842
|
+
decoder.init(asArrayBuffer(bytes));
|
|
1843
|
+
decoder.start();
|
|
1844
|
+
const state = createResizeState(info.width, info.height, target.width, target.height);
|
|
1845
|
+
scanlines = (async function* pngRows() {
|
|
1846
|
+
try {
|
|
1847
|
+
for (const scanline of decoder.readAllScanlines()) {
|
|
1848
|
+
for (const outScanline of processScanline(state, scanline.data, scanline.y)) {
|
|
1849
|
+
yield outScanline;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
for (const outScanline of flushResize(state)) {
|
|
1853
|
+
yield outScanline;
|
|
1854
|
+
}
|
|
1855
|
+
} finally {
|
|
1856
|
+
decoder.dispose();
|
|
1857
|
+
}
|
|
1858
|
+
})();
|
|
1859
|
+
} else {
|
|
1860
|
+
const decoder = await createDecoder(info.format, asArrayBuffer(bytes));
|
|
1861
|
+
const decoded = await decoder.decode();
|
|
1862
|
+
decoder.dispose();
|
|
1863
|
+
const state = createResizeState(decoded.width, decoded.height, target.width, target.height);
|
|
1864
|
+
scanlines = (async function* bufferedRows() {
|
|
1865
|
+
for await (const row of iterateUint8ArrayRows(decoded.pixels, decoded.width, decoded.height)) {
|
|
1866
|
+
for (const outScanline of processScanline(state, row.data, row.y)) {
|
|
1867
|
+
yield outScanline;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
for (const outScanline of flushResize(state)) {
|
|
1871
|
+
yield outScanline;
|
|
1872
|
+
}
|
|
1873
|
+
})();
|
|
1874
|
+
}
|
|
1875
|
+
infoDeferred.resolve({
|
|
1876
|
+
width: target.width,
|
|
1877
|
+
height: target.height,
|
|
1878
|
+
mimeType: "image/jpeg",
|
|
1879
|
+
originalFormat: info.format
|
|
1880
|
+
});
|
|
1129
1881
|
try {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1882
|
+
encoder.init(target.width, target.height, options.quality ?? DEFAULT_QUALITY);
|
|
1883
|
+
encoder.start();
|
|
1884
|
+
for await (const scanline of scanlines) {
|
|
1885
|
+
encoder.writeScanline(scanline);
|
|
1886
|
+
const codecBytes = bytes.byteLength + encoder.getBufferedOutputSize() + encoder.getRowBufferSize();
|
|
1887
|
+
stats.update(bytes.byteLength, encoder.getBufferedOutputSize(), codecBytes, codecBytes);
|
|
1888
|
+
for (const chunk of encoder.drainChunks()) {
|
|
1889
|
+
stats.addBytesOut(chunk.byteLength);
|
|
1890
|
+
stats.update(bytes.byteLength, encoder.getBufferedOutputSize(), codecBytes, codecBytes);
|
|
1891
|
+
yield chunk;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
for (const chunk of encoder.finish()) {
|
|
1895
|
+
stats.addBytesOut(chunk.byteLength);
|
|
1896
|
+
const codecBytes = bytes.byteLength + encoder.getBufferedOutputSize() + encoder.getRowBufferSize();
|
|
1897
|
+
stats.update(bytes.byteLength, encoder.getBufferedOutputSize(), codecBytes, codecBytes);
|
|
1898
|
+
yield chunk;
|
|
1899
|
+
}
|
|
1900
|
+
} finally {
|
|
1901
|
+
encoder.dispose();
|
|
1134
1902
|
}
|
|
1135
1903
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1904
|
+
function transform(input, options = {}) {
|
|
1905
|
+
const infoDeferred = createDeferred();
|
|
1906
|
+
const statsDeferred = createDeferred();
|
|
1907
|
+
const iteratorFactory = () => (async function* transformIterator() {
|
|
1908
|
+
const prepared = await prepareInputSource(input);
|
|
1909
|
+
const info = await inspectSource(prepared);
|
|
1910
|
+
if (info.format === "unknown") {
|
|
1911
|
+
throw new Error("Unsupported image format");
|
|
1912
|
+
}
|
|
1913
|
+
const stats = new StatsTracker(
|
|
1914
|
+
prepared.kind === "stream" ? "streaming-input" : "byte-input"
|
|
1915
|
+
);
|
|
1916
|
+
try {
|
|
1917
|
+
if (info.format === "jpeg") {
|
|
1918
|
+
yield* runJpegTransform(prepared, info, options, infoDeferred, stats);
|
|
1919
|
+
} else {
|
|
1920
|
+
yield* runBufferedTransform(prepared, info, options, infoDeferred, stats);
|
|
1921
|
+
}
|
|
1922
|
+
statsDeferred.resolve(stats.snapshot());
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
infoDeferred.reject(error);
|
|
1925
|
+
statsDeferred.reject(error);
|
|
1926
|
+
throw error;
|
|
1927
|
+
}
|
|
1928
|
+
})();
|
|
1929
|
+
return createEncodedImage(iteratorFactory, infoDeferred.promise, statsDeferred.promise);
|
|
1930
|
+
}
|
|
1931
|
+
async function ready(options = {}) {
|
|
1932
|
+
if (options.wasm instanceof WebAssembly.Module) {
|
|
1933
|
+
await initWithWasmModule(options.wasm);
|
|
1934
|
+
return;
|
|
1154
1935
|
}
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1936
|
+
if (options.wasm instanceof ArrayBuffer) {
|
|
1937
|
+
const compiled = await WebAssembly.compile(options.wasm);
|
|
1938
|
+
await initWithWasmModule(compiled);
|
|
1939
|
+
return;
|
|
1157
1940
|
}
|
|
1158
1941
|
await loadWasm();
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
target.width,
|
|
1173
|
-
target.height
|
|
1174
|
-
);
|
|
1175
|
-
let quality = opts.quality;
|
|
1176
|
-
let jpegData = await encodeToJpeg(resizedPixels, target.width, target.height, quality);
|
|
1177
|
-
while (jpegData.byteLength > opts.maxBytes && quality > 45) {
|
|
1178
|
-
quality -= 10;
|
|
1179
|
-
jpegData = await encodeToJpeg(resizedPixels, target.width, target.height, quality);
|
|
1180
|
-
}
|
|
1181
|
-
if (jpegData.byteLength > opts.maxBytes) {
|
|
1182
|
-
const scaleFactor = Math.sqrt(opts.maxBytes / jpegData.byteLength) * 0.9;
|
|
1183
|
-
const newWidth = Math.round(target.width * scaleFactor);
|
|
1184
|
-
const newHeight = Math.round(target.height * scaleFactor);
|
|
1185
|
-
const smallerPixels = resizePixelBuffer(
|
|
1186
|
-
resizedPixels,
|
|
1187
|
-
target.width,
|
|
1188
|
-
target.height,
|
|
1189
|
-
newWidth,
|
|
1190
|
-
newHeight
|
|
1191
|
-
);
|
|
1192
|
-
jpegData = await encodeToJpeg(smallerPixels, newWidth, newHeight, quality);
|
|
1193
|
-
return {
|
|
1194
|
-
data: jpegData,
|
|
1195
|
-
width: newWidth,
|
|
1196
|
-
height: newHeight,
|
|
1197
|
-
mimeType: "image/jpeg",
|
|
1198
|
-
originalFormat: format
|
|
1199
|
-
};
|
|
1942
|
+
}
|
|
1943
|
+
async function collect(image) {
|
|
1944
|
+
const chunks = [];
|
|
1945
|
+
let total = 0;
|
|
1946
|
+
for await (const chunk of image) {
|
|
1947
|
+
chunks.push(chunk);
|
|
1948
|
+
total += chunk.byteLength;
|
|
1949
|
+
}
|
|
1950
|
+
const merged = new Uint8Array(total);
|
|
1951
|
+
let offset = 0;
|
|
1952
|
+
for (const chunk of chunks) {
|
|
1953
|
+
merged.set(chunk, offset);
|
|
1954
|
+
offset += chunk.byteLength;
|
|
1200
1955
|
}
|
|
1201
1956
|
return {
|
|
1202
|
-
data:
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
mimeType: "image/jpeg",
|
|
1206
|
-
originalFormat: format
|
|
1957
|
+
data: merged.buffer.slice(merged.byteOffset, merged.byteOffset + merged.byteLength),
|
|
1958
|
+
info: await image.info,
|
|
1959
|
+
stats: await image.stats
|
|
1207
1960
|
};
|
|
1208
1961
|
}
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
for (const scanline of remaining) {
|
|
1225
|
-
outputRows[scanline.y] = scanline.data;
|
|
1226
|
-
}
|
|
1227
|
-
const dstRowSize = dstWidth * 3;
|
|
1228
|
-
const result = new Uint8Array(dstWidth * dstHeight * 3);
|
|
1229
|
-
for (let y = 0; y < dstHeight; y++) {
|
|
1230
|
-
if (outputRows[y]) {
|
|
1231
|
-
result.set(outputRows[y], y * dstRowSize);
|
|
1962
|
+
function toReadableStream(image) {
|
|
1963
|
+
const iterator = image[Symbol.asyncIterator]();
|
|
1964
|
+
return new ReadableStream({
|
|
1965
|
+
async pull(controller) {
|
|
1966
|
+
const { value, done } = await iterator.next();
|
|
1967
|
+
if (done) {
|
|
1968
|
+
controller.close();
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
controller.enqueue(value);
|
|
1972
|
+
},
|
|
1973
|
+
async cancel(reason) {
|
|
1974
|
+
if (typeof iterator.return === "function") {
|
|
1975
|
+
await iterator.return(reason);
|
|
1976
|
+
}
|
|
1232
1977
|
}
|
|
1233
|
-
}
|
|
1234
|
-
return result;
|
|
1978
|
+
});
|
|
1235
1979
|
}
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1980
|
+
function toResponse(image, init = {}) {
|
|
1981
|
+
const headers = new Headers(init.headers);
|
|
1982
|
+
headers.set("Content-Type", "image/jpeg");
|
|
1983
|
+
return new Response(toReadableStream(image), {
|
|
1984
|
+
...init,
|
|
1985
|
+
headers
|
|
1986
|
+
});
|
|
1241
1987
|
}
|
|
1242
1988
|
|
|
1243
|
-
|
|
1244
|
-
var sip = {
|
|
1245
|
-
process: process2,
|
|
1246
|
-
probe,
|
|
1247
|
-
detectImageFormat,
|
|
1248
|
-
initStreaming,
|
|
1249
|
-
isStreamingAvailable
|
|
1250
|
-
};
|
|
1251
|
-
|
|
1252
|
-
export { WasmJpegDecoder, WasmJpegEncoder, WasmPngDecoder, calculateDctScaleFactor, calculateOptimalScale, calculateTargetDimensions, createResizeState, detectImageFormat, flushResize, getWasmModule, initStreaming, initWithWasmModule, isStreamingAvailable, isWasmAvailable, loadWasm, probe, process2 as process, processJpegStreaming, processScanline, sip };
|
|
1989
|
+
export { collect, decode, encodeJpeg, inspect, ready, resize, toReadableStream, toResponse, transform };
|
|
1253
1990
|
//# sourceMappingURL=index.js.map
|
|
1254
1991
|
//# sourceMappingURL=index.js.map
|