@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/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/decoders/simple.ts
167
- function isNode() {
168
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
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
- async function initCodecForNode(initFn, wasmPath) {
171
- const { readFile } = await import('fs/promises');
172
- const { createRequire } = await import('module');
173
- const require2 = createRequire(import.meta.url);
174
- const resolvedPath = require2.resolve(wasmPath);
175
- const wasmBuffer = await readFile(resolvedPath);
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
- async init(data) {
194
- this.data = data;
195
- switch (this.format) {
196
- case "avif": {
197
- const { default: decode, init } = await import('@jsquash/avif/decode.js');
198
- if (isNode()) {
199
- await initCodecForNode(init, "@jsquash/avif/codec/dec/avif_dec.wasm");
200
- }
201
- this.decodeFn = decode;
202
- this.hasAlpha = true;
203
- break;
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
- case "webp": {
206
- const { default: decode, init } = await import('@jsquash/webp/decode.js');
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
- this.width = imageData.width;
227
- this.height = imageData.height;
228
- return {
229
- width: this.width,
230
- height: this.height,
231
- hasAlpha: this.hasAlpha
232
- };
343
+ } finally {
344
+ reader.releaseLock();
233
345
  }
234
- async decode(_scaleFactor) {
235
- if (!this.decodeFn) {
236
- throw new Error("Decoder not initialized. Call init() first.");
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 imageData = await this.decodeFn(this.data);
239
- this.width = imageData.width;
240
- this.height = imageData.height;
241
- const rgba = new Uint8Array(imageData.data.buffer);
242
- const rgb = new Uint8Array(this.width * this.height * 3);
243
- let srcIdx = 0;
244
- let dstIdx = 0;
245
- const pixelCount = this.width * this.height;
246
- for (let i = 0; i < pixelCount; i++) {
247
- rgb[dstIdx++] = rgba[srcIdx++];
248
- rgb[dstIdx++] = rgba[srcIdx++];
249
- rgb[dstIdx++] = rgba[srcIdx++];
250
- srcIdx++;
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
- dispose() {
259
- this.decodeFn = null;
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
- async function createDecoder(format, data) {
263
- const decoder = new SimpleDecoder(format, data);
264
- await decoder.init(data);
265
- return decoder;
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
- // src/wasm/loader.ts
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 initWithWasmModule(compiledModule) {
276
- if (wasmModule) {
277
- return;
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 (compiledModule) {
280
- precompiledWasmModule = compiledModule;
491
+ if (input instanceof ArrayBuffer || input instanceof Uint8Array) {
492
+ return new BytesInputSource(toUint8Array(input));
281
493
  }
282
- await loadWasm();
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 getWasmModule() {
285
- if (!wasmModule) {
286
- throw new Error("WASM module not loaded. Call loadWasm() first.");
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 wasmModule;
514
+ return { info, source };
289
515
  }
290
- async function loadWasm() {
291
- if (wasmModule) {
292
- return wasmModule;
516
+ async function inspectSource(source) {
517
+ let best = probe(source.headerBytes);
518
+ if (best.format !== "unknown") {
519
+ return best;
293
520
  }
294
- if (wasmPromise) {
295
- return wasmPromise;
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
- wasmPromise = doLoadWasm();
298
- try {
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 doLoadWasm() {
307
- if (typeof globalThis !== "undefined" && globalThis.__SIP_WASM_LOADER__) {
308
- const loader = globalThis.__SIP_WASM_LOADER__;
309
- return await loader();
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 packages/sip. Error: " + (err instanceof Error ? err.message : String(err))
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
- * Initialize decoder with JPEG data
789
+ * Compatibility helper for full-buffer callers.
377
790
  */
378
791
  init(data) {
379
- const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
380
- this.decoder = this.module._sip_decoder_create();
381
- if (!this.decoder) {
382
- throw new Error("Failed to create JPEG decoder");
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
- this.dataPtr = copyToWasm(this.module, bytes);
385
- if (this.module._sip_decoder_set_source(this.decoder, this.dataPtr, bytes.length) !== 0) {
386
- this.dispose();
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
- if (this.module._sip_decoder_read_header(this.decoder) !== 0) {
390
- this.dispose();
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 { width: this.width, height: this.height };
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
- if (!this.decoder) {
442
- throw new Error("Decoder not initialized");
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
- throw new Error("Decoding already started");
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 (this.module._sip_decoder_start(this.decoder) !== 0) {
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 < 0) {
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
- this.module.HEAPU8.buffer,
477
- this.rowBufferPtr,
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
- * Decode entire image to RGB buffer
499
- *
500
- * @returns Full RGB pixel buffer
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
- const pixels = new Uint8Array(this.outputWidth * this.outputHeight * 3);
507
- const rowSize = this.outputWidth * 3;
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
- pixels,
513
- width: this.outputWidth,
514
- height: this.outputHeight
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
- if (this.dataPtr) {
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
- if (this.module._sip_encoder_init(this.encoder, width, height, this.quality) !== 0) {
576
- this.dispose();
577
- throw new Error("Failed to initialize encoder");
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
- throw new Error("Encoding already started");
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 writing");
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.length !== expectedSize) {
622
- throw new Error(`Invalid scanline size: expected ${expectedSize}, got ${data.length}`);
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
- * Get current scanline number
632
- */
633
- getCurrentLine() {
634
- return this.currentLine;
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 encoding");
1006
+ throw new Error("Failed to finish JPEG compression");
650
1007
  }
651
1008
  this.finished = true;
652
- const outputPtr = this.module._sip_encoder_get_output(this.encoder);
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
- const rowData = pixels.subarray(y * rowSize, (y + 1) * rowSize);
674
- this.writeScanlineData(rowData);
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
- return this.finish();
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/encoder.ts
848
- var NativeEncoder = class {
849
- supportsScanline = true;
850
- width = 0;
851
- height = 0;
852
- quality = 85;
853
- wasmEncoder = null;
854
- async init(width, height, quality) {
855
- this.width = width;
856
- this.height = height;
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
- srcWidth,
885
- srcHeight,
886
- dstWidth,
887
- dstHeight,
888
- bufferA: null,
889
- bufferB: null,
890
- bufferAY: -1,
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 resizeRowHorizontal(src, srcWidth, dstWidth) {
896
- const dst = new Uint8Array(dstWidth * 3);
897
- const xScale = srcWidth / dstWidth;
898
- for (let dstX = 0; dstX < dstWidth; dstX++) {
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
- return dst;
912
- }
913
- function blendRows(rowA, rowB, t, width) {
914
- const result = new Uint8Array(width * 3);
915
- const invT = 1 - t;
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 result;
1243
+ return merged;
920
1244
  }
921
- function processScanline(state, srcScanline, srcY) {
922
- const { srcWidth, srcHeight, dstWidth, dstHeight } = state;
923
- const yScale = srcHeight / dstHeight;
924
- const output = [];
925
- const resizedRow = resizeRowHorizontal(srcScanline, srcWidth, dstWidth);
926
- state.bufferA = state.bufferB;
927
- state.bufferAY = state.bufferBY;
928
- state.bufferB = resizedRow;
929
- state.bufferBY = srcY;
930
- while (state.currentOutputY < dstHeight) {
931
- const srcYFloat = state.currentOutputY * yScale;
932
- const srcYFloor = Math.floor(srcYFloat);
933
- const srcYCeil = Math.min(srcYFloor + 1, srcHeight - 1);
934
- if (srcYCeil > srcY) {
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
- if (state.bufferA === null) {
938
- output.push({
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
- const t = srcYFloat - srcYFloor;
947
- let rowA = state.bufferA;
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
- const blended = blendRows(rowA, rowB, t, dstWidth);
957
- output.push({
958
- data: blended,
959
- width: dstWidth,
960
- y: state.currentOutputY
961
- });
962
- state.currentOutputY++;
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 output;
1315
+ return null;
965
1316
  }
966
- function flushResize(state) {
967
- const output = [];
968
- while (state.currentOutputY < state.dstHeight) {
969
- if (state.bufferB === null) break;
970
- output.push({
971
- data: state.bufferB,
972
- width: state.dstWidth,
973
- y: state.currentOutputY
974
- });
975
- state.currentOutputY++;
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
- return output;
1380
+ const extended = await source.ensureHeaderBytes(262144);
1381
+ return readJpegOrientation(extended);
978
1382
  }
979
- function calculateTargetDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
980
- const scaleX = maxWidth / srcWidth;
981
- const scaleY = maxHeight / srcHeight;
982
- const scale = Math.min(scaleX, scaleY, 1);
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
- width: Math.round(srcWidth * scale),
985
- height: Math.round(srcHeight * scale),
986
- scale
1421
+ info,
1422
+ stats,
1423
+ [Symbol.asyncIterator]() {
1424
+ return iteratorFactory()[Symbol.asyncIterator]();
1425
+ }
987
1426
  };
988
1427
  }
989
- function calculateDctScaleFactor(srcWidth, srcHeight, targetWidth, targetHeight) {
990
- const scales = [8, 4, 2, 1];
991
- for (const scale of scales) {
992
- const scaledWidth = Math.ceil(srcWidth / scale);
993
- const scaledHeight = Math.ceil(srcHeight / scale);
994
- if (scaledWidth >= targetWidth && scaledHeight >= targetHeight) {
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
- // src/streaming.ts
1002
- var DEFAULT_OPTIONS = {
1003
- maxWidth: 4096,
1004
- maxHeight: 4096,
1005
- maxBytes: 1.5 * 1024 * 1024,
1006
- quality: 85
1007
- };
1008
- async function processJpegStreaming(input, options = {}) {
1009
- const opts = { ...DEFAULT_OPTIONS, ...options };
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
- const decoder = new WasmJpegDecoder();
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 { width: srcWidth, height: srcHeight } = decoder.init(input);
1014
- const target = calculateTargetDimensions(
1015
- srcWidth,
1016
- srcHeight,
1017
- opts.maxWidth,
1018
- opts.maxHeight
1019
- );
1020
- const dctScale = calculateOptimalScale(
1021
- srcWidth,
1022
- srcHeight,
1023
- target.width,
1024
- target.height
1025
- );
1026
- const { width: decodeWidth, height: decodeHeight } = decoder.setScale(dctScale);
1027
- const resizeState = createResizeState(
1028
- decodeWidth,
1029
- decodeHeight,
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 encoder = new WasmJpegEncoder();
1034
- encoder.init(target.width, target.height, opts.quality);
1035
- encoder.start();
1036
- decoder.start();
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 remaining = flushResize(resizeState);
1046
- for (const outScanline of remaining) {
1047
- encoder.writeScanline(outScanline);
1619
+ for (const next of flushResize(state)) {
1620
+ yield next;
1048
1621
  }
1049
- const jpegData = encoder.finish();
1050
- if (jpegData.byteLength > opts.maxBytes && opts.quality > 45) {
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
- encoder.dispose();
1059
- return {
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 processPngStreaming(input, options = {}) {
1071
- const opts = { ...DEFAULT_OPTIONS, ...options };
1656
+ async function* runJpegTransform(source, info, options, infoDeferred, stats) {
1072
1657
  await loadWasm();
1073
- const decoder = new WasmPngDecoder();
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
- const { width: srcWidth, height: srcHeight } = decoder.init(input);
1076
- const target = calculateTargetDimensions(
1077
- srcWidth,
1078
- srcHeight,
1079
- opts.maxWidth,
1080
- opts.maxHeight
1081
- );
1082
- const resizeState = createResizeState(
1083
- srcWidth,
1084
- srcHeight,
1085
- target.width,
1086
- target.height
1087
- );
1088
- const encoder = new WasmJpegEncoder();
1089
- encoder.init(target.width, target.height, opts.quality);
1090
- encoder.start();
1091
- decoder.start();
1092
- let decodedLine = 0;
1093
- for (const scanline of decoder.readAllScanlines()) {
1094
- const outputScanlines = processScanline(resizeState, scanline.data, decodedLine);
1095
- decodedLine++;
1096
- for (const outScanline of outputScanlines) {
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
- const remaining = flushResize(resizeState);
1101
- for (const outScanline of remaining) {
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 jpegData = encoder.finish();
1105
- if (jpegData.byteLength > opts.maxBytes && opts.quality > 45) {
1106
- encoder.dispose();
1107
- decoder.dispose();
1108
- return processPngStreaming(input, {
1109
- ...opts,
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 isStreamingAvailable() {
1126
- return isWasmAvailable();
1127
- }
1128
- async function initStreaming() {
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
- await loadWasm();
1131
- return true;
1132
- } catch {
1133
- return false;
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
- // src/pipeline.ts
1138
- var DEFAULT_OPTIONS2 = {
1139
- maxWidth: 4096,
1140
- maxHeight: 4096,
1141
- maxBytes: 1.5 * 1024 * 1024,
1142
- // 1.5MB
1143
- quality: 85
1144
- };
1145
- async function process2(input, options = {}) {
1146
- const opts = { ...DEFAULT_OPTIONS2, ...options };
1147
- const probeResult = probe(input);
1148
- if (probeResult.format === "unknown") {
1149
- throw new Error("Unknown image format");
1150
- }
1151
- const { format, width: srcWidth, height: srcHeight } = probeResult;
1152
- if (format === "jpeg") {
1153
- return await processJpegStreaming(input, opts);
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 (format === "png") {
1156
- return await processPngStreaming(input, opts);
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
- const target = calculateTargetDimensions(
1160
- srcWidth,
1161
- srcHeight,
1162
- opts.maxWidth,
1163
- opts.maxHeight
1164
- );
1165
- const decoder = await createDecoder(format, input);
1166
- const { pixels: srcPixels, width: decodedWidth, height: decodedHeight } = await decoder.decode();
1167
- decoder.dispose();
1168
- const resizedPixels = resizePixelBuffer(
1169
- srcPixels,
1170
- decodedWidth,
1171
- decodedHeight,
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: jpegData,
1203
- width: target.width,
1204
- height: target.height,
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 resizePixelBuffer(srcPixels, srcWidth, srcHeight, dstWidth, dstHeight) {
1210
- if (srcWidth === dstWidth && srcHeight === dstHeight) {
1211
- return srcPixels;
1212
- }
1213
- const state = createResizeState(srcWidth, srcHeight, dstWidth, dstHeight);
1214
- const outputRows = new Array(dstHeight);
1215
- const srcRowSize = srcWidth * 3;
1216
- for (let y = 0; y < srcHeight; y++) {
1217
- const srcRow = srcPixels.subarray(y * srcRowSize, (y + 1) * srcRowSize);
1218
- const outputScanlines = processScanline(state, srcRow, y);
1219
- for (const scanline of outputScanlines) {
1220
- outputRows[scanline.y] = scanline.data;
1221
- }
1222
- }
1223
- const remaining = flushResize(state);
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
- async function encodeToJpeg(pixels, width, height, quality) {
1237
- const encoder = await createEncoder(width, height, quality);
1238
- const result = await encoder.encode(pixels);
1239
- encoder.dispose();
1240
- return result;
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
- // src/index.ts
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