@standardagents/sip 0.10.0-dev
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 +170 -0
- package/dist/index.d.ts +615 -0
- package/dist/index.js +1272 -0
- package/dist/index.js.map +1 -0
- package/dist/sip.js +2 -0
- package/dist/sip.wasm +0 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
// src/probe.ts
|
|
2
|
+
var MAGIC = {
|
|
3
|
+
// JPEG: FFD8FF
|
|
4
|
+
JPEG: [255, 216, 255],
|
|
5
|
+
// PNG: 89504E47 0D0A1A0A
|
|
6
|
+
PNG: [137, 80, 78, 71, 13, 10, 26, 10],
|
|
7
|
+
// WebP: RIFF....WEBP
|
|
8
|
+
RIFF: [82, 73, 70, 70],
|
|
9
|
+
// "RIFF"
|
|
10
|
+
WEBP: [87, 69, 66, 80],
|
|
11
|
+
// "WEBP"
|
|
12
|
+
// AVIF: ....ftypavif or ....ftypavis
|
|
13
|
+
FTYP: [102, 116, 121, 112]
|
|
14
|
+
// "ftyp"
|
|
15
|
+
};
|
|
16
|
+
function detectFormat(data) {
|
|
17
|
+
if (data.length < 12) return "unknown";
|
|
18
|
+
if (data[0] === MAGIC.JPEG[0] && data[1] === MAGIC.JPEG[1] && data[2] === MAGIC.JPEG[2]) {
|
|
19
|
+
return "jpeg";
|
|
20
|
+
}
|
|
21
|
+
if (data[0] === MAGIC.PNG[0] && data[1] === MAGIC.PNG[1] && data[2] === MAGIC.PNG[2] && data[3] === MAGIC.PNG[3] && data[4] === MAGIC.PNG[4] && data[5] === MAGIC.PNG[5] && data[6] === MAGIC.PNG[6] && data[7] === MAGIC.PNG[7]) {
|
|
22
|
+
return "png";
|
|
23
|
+
}
|
|
24
|
+
if (data[0] === MAGIC.RIFF[0] && data[1] === MAGIC.RIFF[1] && data[2] === MAGIC.RIFF[2] && data[3] === MAGIC.RIFF[3] && data[8] === MAGIC.WEBP[0] && data[9] === MAGIC.WEBP[1] && data[10] === MAGIC.WEBP[2] && data[11] === MAGIC.WEBP[3]) {
|
|
25
|
+
return "webp";
|
|
26
|
+
}
|
|
27
|
+
if (data[4] === MAGIC.FTYP[0] && data[5] === MAGIC.FTYP[1] && data[6] === MAGIC.FTYP[2] && data[7] === MAGIC.FTYP[3]) {
|
|
28
|
+
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
|
|
29
|
+
if (brand === "avif" || brand === "avis" || brand === "mif1" || brand === "msf1") {
|
|
30
|
+
return "avif";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return "unknown";
|
|
34
|
+
}
|
|
35
|
+
function probeJpeg(data) {
|
|
36
|
+
let offset = 2;
|
|
37
|
+
while (offset < data.length - 1) {
|
|
38
|
+
if (data[offset] !== 255) {
|
|
39
|
+
offset++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
while (offset < data.length && data[offset] === 255) {
|
|
43
|
+
offset++;
|
|
44
|
+
}
|
|
45
|
+
if (offset >= data.length) break;
|
|
46
|
+
const marker = data[offset++];
|
|
47
|
+
const isSOF = marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207;
|
|
48
|
+
if (isSOF) {
|
|
49
|
+
if (offset + 7 > data.length) return null;
|
|
50
|
+
const height = data[offset + 3] << 8 | data[offset + 4];
|
|
51
|
+
const width = data[offset + 5] << 8 | data[offset + 6];
|
|
52
|
+
return { width, height };
|
|
53
|
+
}
|
|
54
|
+
if (marker === 216 || marker === 217 || marker >= 208 && marker <= 215) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (offset + 1 >= data.length) break;
|
|
58
|
+
const segmentLength = data[offset] << 8 | data[offset + 1];
|
|
59
|
+
offset += segmentLength;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function probePng(data) {
|
|
64
|
+
if (data.length < 24) return null;
|
|
65
|
+
const chunkType = String.fromCharCode(data[12], data[13], data[14], data[15]);
|
|
66
|
+
if (chunkType !== "IHDR") return null;
|
|
67
|
+
const width = data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19];
|
|
68
|
+
const height = data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23];
|
|
69
|
+
const colorType = data[25];
|
|
70
|
+
const hasAlpha = colorType === 4 || colorType === 6;
|
|
71
|
+
return { width, height, hasAlpha };
|
|
72
|
+
}
|
|
73
|
+
function probeWebp(data) {
|
|
74
|
+
if (data.length < 30) return null;
|
|
75
|
+
const chunkType = String.fromCharCode(data[12], data[13], data[14], data[15]);
|
|
76
|
+
if (chunkType === "VP8 ") {
|
|
77
|
+
if (data.length < 30) return null;
|
|
78
|
+
if (data[23] !== 157 || data[24] !== 1 || data[25] !== 42) return null;
|
|
79
|
+
const width = (data[26] | data[27] << 8) & 16383;
|
|
80
|
+
const height = (data[28] | data[29] << 8) & 16383;
|
|
81
|
+
return { width, height, hasAlpha: false };
|
|
82
|
+
}
|
|
83
|
+
if (chunkType === "VP8L") {
|
|
84
|
+
if (data[20] !== 47) return null;
|
|
85
|
+
const bits = data[21] | data[22] << 8 | data[23] << 16 | data[24] << 24;
|
|
86
|
+
const width = (bits & 16383) + 1;
|
|
87
|
+
const height = (bits >> 14 & 16383) + 1;
|
|
88
|
+
const hasAlpha = (bits >> 28 & 1) === 1;
|
|
89
|
+
return { width, height, hasAlpha };
|
|
90
|
+
}
|
|
91
|
+
if (chunkType === "VP8X") {
|
|
92
|
+
const flags = data[20];
|
|
93
|
+
const hasAlpha = (flags & 16) !== 0;
|
|
94
|
+
const width = (data[24] | data[25] << 8 | data[26] << 16) + 1;
|
|
95
|
+
const height = (data[27] | data[28] << 8 | data[29] << 16) + 1;
|
|
96
|
+
return { width, height, hasAlpha };
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function probeAvif(data) {
|
|
101
|
+
let offset = 0;
|
|
102
|
+
while (offset + 8 <= data.length) {
|
|
103
|
+
const size = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
|
|
104
|
+
const type = String.fromCharCode(
|
|
105
|
+
data[offset + 4],
|
|
106
|
+
data[offset + 5],
|
|
107
|
+
data[offset + 6],
|
|
108
|
+
data[offset + 7]
|
|
109
|
+
);
|
|
110
|
+
if (size === 0) break;
|
|
111
|
+
if (size < 8) break;
|
|
112
|
+
if (type === "ispe" && offset + 20 <= data.length) {
|
|
113
|
+
const width = data[offset + 12] << 24 | data[offset + 13] << 16 | data[offset + 14] << 8 | data[offset + 15];
|
|
114
|
+
const height = data[offset + 16] << 24 | data[offset + 17] << 16 | data[offset + 18] << 8 | data[offset + 19];
|
|
115
|
+
if (width > 0 && height > 0) {
|
|
116
|
+
return { width, height };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (type === "meta" || type === "iprp" || type === "ipco") {
|
|
120
|
+
const headerSize = type === "meta" ? 12 : 8;
|
|
121
|
+
offset += headerSize;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
offset += size;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function probe(input) {
|
|
129
|
+
const data = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
|
|
130
|
+
const format = detectFormat(data);
|
|
131
|
+
let result = null;
|
|
132
|
+
switch (format) {
|
|
133
|
+
case "jpeg":
|
|
134
|
+
result = probeJpeg(data);
|
|
135
|
+
break;
|
|
136
|
+
case "png":
|
|
137
|
+
result = probePng(data);
|
|
138
|
+
break;
|
|
139
|
+
case "webp":
|
|
140
|
+
result = probeWebp(data);
|
|
141
|
+
break;
|
|
142
|
+
case "avif":
|
|
143
|
+
result = probeAvif(data);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (!result) {
|
|
147
|
+
return {
|
|
148
|
+
format: "unknown",
|
|
149
|
+
width: 0,
|
|
150
|
+
height: 0,
|
|
151
|
+
hasAlpha: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
format,
|
|
156
|
+
width: result.width,
|
|
157
|
+
height: result.height,
|
|
158
|
+
hasAlpha: result.hasAlpha ?? false
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function detectImageFormat(input) {
|
|
162
|
+
const data = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
|
|
163
|
+
return detectFormat(data);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/decoders/simple.ts
|
|
167
|
+
function isNode() {
|
|
168
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
169
|
+
}
|
|
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;
|
|
192
|
+
}
|
|
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;
|
|
204
|
+
}
|
|
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;
|
|
213
|
+
}
|
|
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
|
+
}
|
|
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
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async decode(_scaleFactor) {
|
|
235
|
+
if (!this.decodeFn) {
|
|
236
|
+
throw new Error("Decoder not initialized. Call init() first.");
|
|
237
|
+
}
|
|
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++;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
pixels: rgb,
|
|
254
|
+
width: this.width,
|
|
255
|
+
height: this.height
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
dispose() {
|
|
259
|
+
this.decodeFn = null;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
async function createDecoder(format, data) {
|
|
263
|
+
const decoder = new SimpleDecoder(format, data);
|
|
264
|
+
await decoder.init(data);
|
|
265
|
+
return decoder;
|
|
266
|
+
}
|
|
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;
|
|
274
|
+
}
|
|
275
|
+
async function initWithWasmModule(compiledModule) {
|
|
276
|
+
console.log("[sip:initWithWasmModule] Called with module:", compiledModule ? "provided" : "none");
|
|
277
|
+
if (wasmModule) {
|
|
278
|
+
console.log("[sip:initWithWasmModule] Already initialized, skipping");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (compiledModule) {
|
|
282
|
+
precompiledWasmModule = compiledModule;
|
|
283
|
+
console.log("[sip:initWithWasmModule] Stored pre-compiled module");
|
|
284
|
+
}
|
|
285
|
+
console.log("[sip:initWithWasmModule] Calling loadWasm...");
|
|
286
|
+
await loadWasm();
|
|
287
|
+
console.log("[sip:initWithWasmModule] loadWasm completed, wasmModule:", wasmModule ? "loaded" : "null");
|
|
288
|
+
}
|
|
289
|
+
function getWasmModule() {
|
|
290
|
+
if (!wasmModule) {
|
|
291
|
+
throw new Error("WASM module not loaded. Call loadWasm() first.");
|
|
292
|
+
}
|
|
293
|
+
return wasmModule;
|
|
294
|
+
}
|
|
295
|
+
async function loadWasm() {
|
|
296
|
+
if (wasmModule) {
|
|
297
|
+
return wasmModule;
|
|
298
|
+
}
|
|
299
|
+
if (wasmPromise) {
|
|
300
|
+
return wasmPromise;
|
|
301
|
+
}
|
|
302
|
+
wasmPromise = doLoadWasm();
|
|
303
|
+
try {
|
|
304
|
+
wasmModule = await wasmPromise;
|
|
305
|
+
return wasmModule;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
wasmPromise = null;
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function doLoadWasm() {
|
|
312
|
+
console.log("[sip:doLoadWasm] Starting...");
|
|
313
|
+
if (typeof globalThis !== "undefined" && globalThis.__SIP_WASM_LOADER__) {
|
|
314
|
+
console.log("[sip:doLoadWasm] Using external loader");
|
|
315
|
+
const loader = globalThis.__SIP_WASM_LOADER__;
|
|
316
|
+
return await loader();
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
console.log("[sip:doLoadWasm] Importing sip.js...");
|
|
320
|
+
const createSipModule = (await import('./sip.js')).default;
|
|
321
|
+
console.log("[sip:doLoadWasm] sip.js imported, createSipModule:", typeof createSipModule);
|
|
322
|
+
if (precompiledWasmModule) {
|
|
323
|
+
console.log("[sip:doLoadWasm] Using pre-compiled module with instantiateWasm callback");
|
|
324
|
+
const module2 = await new Promise((resolve, reject) => {
|
|
325
|
+
let resolvedModule = null;
|
|
326
|
+
createSipModule({
|
|
327
|
+
instantiateWasm: (imports, receiveInstance) => {
|
|
328
|
+
console.log("[sip:instantiateWasm] Called, instantiating with pre-compiled module");
|
|
329
|
+
WebAssembly.instantiate(precompiledWasmModule, imports).then((instance) => {
|
|
330
|
+
console.log("[sip:instantiateWasm] Instance created successfully");
|
|
331
|
+
receiveInstance(instance);
|
|
332
|
+
}).catch((err) => {
|
|
333
|
+
console.error("[sip:instantiateWasm] Failed:", err);
|
|
334
|
+
reject(err);
|
|
335
|
+
});
|
|
336
|
+
return {};
|
|
337
|
+
},
|
|
338
|
+
onRuntimeInitialized: () => {
|
|
339
|
+
console.log("[sip:onRuntimeInitialized] Runtime ready, HEAPU8:", resolvedModule?.HEAPU8 ? "exists" : "undefined");
|
|
340
|
+
if (resolvedModule && resolvedModule.HEAPU8) {
|
|
341
|
+
resolve(resolvedModule);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}).then((mod) => {
|
|
345
|
+
console.log("[sip:doLoadWasm] Module promise resolved, HEAPU8:", mod?.HEAPU8 ? "exists" : "undefined");
|
|
346
|
+
resolvedModule = mod;
|
|
347
|
+
if (mod.HEAPU8) {
|
|
348
|
+
resolve(mod);
|
|
349
|
+
}
|
|
350
|
+
}).catch(reject);
|
|
351
|
+
});
|
|
352
|
+
return module2;
|
|
353
|
+
}
|
|
354
|
+
console.log("[sip:doLoadWasm] Using standard loading");
|
|
355
|
+
const module = await createSipModule();
|
|
356
|
+
console.log("[sip:doLoadWasm] Standard load complete, module.HEAPU8:", module?.HEAPU8 ? "exists" : "undefined");
|
|
357
|
+
return module;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error("[sip:doLoadWasm] Failed:", err);
|
|
360
|
+
throw new Error(
|
|
361
|
+
"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))
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function copyToWasm(module, data) {
|
|
366
|
+
const ptr = module._malloc(data.length);
|
|
367
|
+
if (!ptr) {
|
|
368
|
+
throw new Error("Failed to allocate WASM memory");
|
|
369
|
+
}
|
|
370
|
+
module.HEAPU8.set(data, ptr);
|
|
371
|
+
return ptr;
|
|
372
|
+
}
|
|
373
|
+
function copyFromWasm(module, ptr, size) {
|
|
374
|
+
return new Uint8Array(module.HEAPU8.buffer, ptr, size).slice();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/wasm/decoder.ts
|
|
378
|
+
var WasmJpegDecoder = class {
|
|
379
|
+
module;
|
|
380
|
+
decoder = 0;
|
|
381
|
+
dataPtr = 0;
|
|
382
|
+
width = 0;
|
|
383
|
+
height = 0;
|
|
384
|
+
outputWidth = 0;
|
|
385
|
+
outputHeight = 0;
|
|
386
|
+
scaleDenom = 1;
|
|
387
|
+
rowBufferPtr = 0;
|
|
388
|
+
started = false;
|
|
389
|
+
finished = false;
|
|
390
|
+
constructor() {
|
|
391
|
+
this.module = getWasmModule();
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Initialize decoder with JPEG data
|
|
395
|
+
*/
|
|
396
|
+
init(data) {
|
|
397
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
398
|
+
this.decoder = this.module._sip_decoder_create();
|
|
399
|
+
if (!this.decoder) {
|
|
400
|
+
throw new Error("Failed to create JPEG decoder");
|
|
401
|
+
}
|
|
402
|
+
this.dataPtr = copyToWasm(this.module, bytes);
|
|
403
|
+
if (this.module._sip_decoder_set_source(this.decoder, this.dataPtr, bytes.length) !== 0) {
|
|
404
|
+
this.dispose();
|
|
405
|
+
throw new Error("Failed to set decoder source");
|
|
406
|
+
}
|
|
407
|
+
if (this.module._sip_decoder_read_header(this.decoder) !== 0) {
|
|
408
|
+
this.dispose();
|
|
409
|
+
throw new Error("Failed to read JPEG header");
|
|
410
|
+
}
|
|
411
|
+
this.width = this.module._sip_decoder_get_width(this.decoder);
|
|
412
|
+
this.height = this.module._sip_decoder_get_height(this.decoder);
|
|
413
|
+
this.outputWidth = this.width;
|
|
414
|
+
this.outputHeight = this.height;
|
|
415
|
+
return { width: this.width, height: this.height };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get original image dimensions
|
|
419
|
+
*/
|
|
420
|
+
getDimensions() {
|
|
421
|
+
return { width: this.width, height: this.height };
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Set DCT scale factor for decoding
|
|
425
|
+
*
|
|
426
|
+
* Must be called after init() and before start()
|
|
427
|
+
*
|
|
428
|
+
* @param scaleDenom - Scale denominator: 1, 2, 4, or 8
|
|
429
|
+
* 1 = full size (default)
|
|
430
|
+
* 2 = 1/2 size
|
|
431
|
+
* 4 = 1/4 size
|
|
432
|
+
* 8 = 1/8 size
|
|
433
|
+
*/
|
|
434
|
+
setScale(scaleDenom) {
|
|
435
|
+
if (!this.decoder) {
|
|
436
|
+
throw new Error("Decoder not initialized");
|
|
437
|
+
}
|
|
438
|
+
if (this.started) {
|
|
439
|
+
throw new Error("Cannot change scale after decoding started");
|
|
440
|
+
}
|
|
441
|
+
if (this.module._sip_decoder_set_scale(this.decoder, scaleDenom) !== 0) {
|
|
442
|
+
throw new Error(`Invalid scale denominator: ${scaleDenom}`);
|
|
443
|
+
}
|
|
444
|
+
this.scaleDenom = scaleDenom;
|
|
445
|
+
this.outputWidth = this.module._sip_decoder_get_output_width(this.decoder);
|
|
446
|
+
this.outputHeight = this.module._sip_decoder_get_output_height(this.decoder);
|
|
447
|
+
return { width: this.outputWidth, height: this.outputHeight };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get output dimensions (after any scaling)
|
|
451
|
+
*/
|
|
452
|
+
getOutputDimensions() {
|
|
453
|
+
return { width: this.outputWidth, height: this.outputHeight };
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Start decoding
|
|
457
|
+
*/
|
|
458
|
+
start() {
|
|
459
|
+
if (!this.decoder) {
|
|
460
|
+
throw new Error("Decoder not initialized");
|
|
461
|
+
}
|
|
462
|
+
if (this.started) {
|
|
463
|
+
throw new Error("Decoding already started");
|
|
464
|
+
}
|
|
465
|
+
if (this.module._sip_decoder_start(this.decoder) !== 0) {
|
|
466
|
+
throw new Error("Failed to start decompression");
|
|
467
|
+
}
|
|
468
|
+
this.rowBufferPtr = this.module._sip_decoder_get_row_buffer(this.decoder);
|
|
469
|
+
if (!this.rowBufferPtr) {
|
|
470
|
+
throw new Error("Failed to get row buffer");
|
|
471
|
+
}
|
|
472
|
+
this.started = true;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Read next scanline
|
|
476
|
+
*
|
|
477
|
+
* @returns Scanline object or null if no more scanlines
|
|
478
|
+
*/
|
|
479
|
+
readScanline() {
|
|
480
|
+
if (!this.started || this.finished) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
const result = this.module._sip_decoder_read_scanline(this.decoder);
|
|
484
|
+
if (result === 0) {
|
|
485
|
+
this.finished = true;
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
if (result < 0) {
|
|
489
|
+
throw new Error("Failed to read scanline");
|
|
490
|
+
}
|
|
491
|
+
const y = this.module._sip_decoder_get_scanline(this.decoder) - 1;
|
|
492
|
+
const rowSize = this.outputWidth * 3;
|
|
493
|
+
const data = new Uint8Array(
|
|
494
|
+
this.module.HEAPU8.buffer,
|
|
495
|
+
this.rowBufferPtr,
|
|
496
|
+
rowSize
|
|
497
|
+
).slice();
|
|
498
|
+
return {
|
|
499
|
+
data,
|
|
500
|
+
width: this.outputWidth,
|
|
501
|
+
y
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Read all remaining scanlines
|
|
506
|
+
*
|
|
507
|
+
* @yields Scanline objects
|
|
508
|
+
*/
|
|
509
|
+
*readAllScanlines() {
|
|
510
|
+
let scanline;
|
|
511
|
+
while ((scanline = this.readScanline()) !== null) {
|
|
512
|
+
yield scanline;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Decode entire image to RGB buffer
|
|
517
|
+
*
|
|
518
|
+
* @returns Full RGB pixel buffer
|
|
519
|
+
*/
|
|
520
|
+
decodeAll() {
|
|
521
|
+
if (!this.started) {
|
|
522
|
+
this.start();
|
|
523
|
+
}
|
|
524
|
+
const pixels = new Uint8Array(this.outputWidth * this.outputHeight * 3);
|
|
525
|
+
const rowSize = this.outputWidth * 3;
|
|
526
|
+
for (const scanline of this.readAllScanlines()) {
|
|
527
|
+
pixels.set(scanline.data, scanline.y * rowSize);
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
pixels,
|
|
531
|
+
width: this.outputWidth,
|
|
532
|
+
height: this.outputHeight
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Clean up resources
|
|
537
|
+
*/
|
|
538
|
+
dispose() {
|
|
539
|
+
if (this.decoder) {
|
|
540
|
+
this.module._sip_decoder_destroy(this.decoder);
|
|
541
|
+
this.decoder = 0;
|
|
542
|
+
}
|
|
543
|
+
if (this.dataPtr) {
|
|
544
|
+
this.module._free(this.dataPtr);
|
|
545
|
+
this.dataPtr = 0;
|
|
546
|
+
}
|
|
547
|
+
this.started = false;
|
|
548
|
+
this.finished = false;
|
|
549
|
+
this.rowBufferPtr = 0;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
function calculateOptimalScale(srcWidth, srcHeight, targetWidth, targetHeight) {
|
|
553
|
+
const scales = [8, 4, 2, 1];
|
|
554
|
+
for (const scale of scales) {
|
|
555
|
+
const scaledWidth = Math.ceil(srcWidth / scale);
|
|
556
|
+
const scaledHeight = Math.ceil(srcHeight / scale);
|
|
557
|
+
if (scaledWidth >= targetWidth && scaledHeight >= targetHeight) {
|
|
558
|
+
return scale;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/wasm/encoder.ts
|
|
565
|
+
var WasmJpegEncoder = class {
|
|
566
|
+
module;
|
|
567
|
+
encoder = 0;
|
|
568
|
+
width = 0;
|
|
569
|
+
height = 0;
|
|
570
|
+
quality = 85;
|
|
571
|
+
rowBufferPtr = 0;
|
|
572
|
+
started = false;
|
|
573
|
+
finished = false;
|
|
574
|
+
currentLine = 0;
|
|
575
|
+
constructor() {
|
|
576
|
+
this.module = getWasmModule();
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Initialize encoder with output dimensions and quality
|
|
580
|
+
*
|
|
581
|
+
* @param width - Output image width
|
|
582
|
+
* @param height - Output image height
|
|
583
|
+
* @param quality - JPEG quality (1-100, default 85)
|
|
584
|
+
*/
|
|
585
|
+
init(width, height, quality = 85) {
|
|
586
|
+
this.width = width;
|
|
587
|
+
this.height = height;
|
|
588
|
+
this.quality = Math.max(1, Math.min(100, quality));
|
|
589
|
+
this.encoder = this.module._sip_encoder_create();
|
|
590
|
+
if (!this.encoder) {
|
|
591
|
+
throw new Error("Failed to create JPEG encoder");
|
|
592
|
+
}
|
|
593
|
+
if (this.module._sip_encoder_init(this.encoder, width, height, this.quality) !== 0) {
|
|
594
|
+
this.dispose();
|
|
595
|
+
throw new Error("Failed to initialize encoder");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Start encoding
|
|
600
|
+
*/
|
|
601
|
+
start() {
|
|
602
|
+
if (!this.encoder) {
|
|
603
|
+
throw new Error("Encoder not initialized");
|
|
604
|
+
}
|
|
605
|
+
if (this.started) {
|
|
606
|
+
throw new Error("Encoding already started");
|
|
607
|
+
}
|
|
608
|
+
if (this.module._sip_encoder_start(this.encoder) !== 0) {
|
|
609
|
+
throw new Error("Failed to start compression");
|
|
610
|
+
}
|
|
611
|
+
this.rowBufferPtr = this.module._sip_encoder_get_row_buffer(this.encoder);
|
|
612
|
+
if (!this.rowBufferPtr) {
|
|
613
|
+
throw new Error("Failed to get row buffer");
|
|
614
|
+
}
|
|
615
|
+
this.started = true;
|
|
616
|
+
this.currentLine = 0;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Write a scanline to the encoder
|
|
620
|
+
*
|
|
621
|
+
* @param scanline - Scanline with RGB data
|
|
622
|
+
*/
|
|
623
|
+
writeScanline(scanline) {
|
|
624
|
+
this.writeScanlineData(scanline.data);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Write raw RGB data as a scanline
|
|
628
|
+
*
|
|
629
|
+
* @param data - RGB data (width * 3 bytes)
|
|
630
|
+
*/
|
|
631
|
+
writeScanlineData(data) {
|
|
632
|
+
if (!this.started || this.finished) {
|
|
633
|
+
throw new Error("Encoder not ready for writing");
|
|
634
|
+
}
|
|
635
|
+
if (this.currentLine >= this.height) {
|
|
636
|
+
throw new Error("All scanlines already written");
|
|
637
|
+
}
|
|
638
|
+
const expectedSize = this.width * 3;
|
|
639
|
+
if (data.length !== expectedSize) {
|
|
640
|
+
throw new Error(`Invalid scanline size: expected ${expectedSize}, got ${data.length}`);
|
|
641
|
+
}
|
|
642
|
+
this.module.HEAPU8.set(data, this.rowBufferPtr);
|
|
643
|
+
if (this.module._sip_encoder_write_scanline(this.encoder) !== 1) {
|
|
644
|
+
throw new Error("Failed to write scanline");
|
|
645
|
+
}
|
|
646
|
+
this.currentLine++;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get current scanline number
|
|
650
|
+
*/
|
|
651
|
+
getCurrentLine() {
|
|
652
|
+
return this.currentLine;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Finish encoding and get output
|
|
656
|
+
*
|
|
657
|
+
* @returns JPEG data as ArrayBuffer
|
|
658
|
+
*/
|
|
659
|
+
finish() {
|
|
660
|
+
if (!this.started) {
|
|
661
|
+
throw new Error("Encoding not started");
|
|
662
|
+
}
|
|
663
|
+
if (this.currentLine !== this.height) {
|
|
664
|
+
throw new Error(`Incomplete image: wrote ${this.currentLine}/${this.height} scanlines`);
|
|
665
|
+
}
|
|
666
|
+
if (this.module._sip_encoder_finish(this.encoder) !== 0) {
|
|
667
|
+
throw new Error("Failed to finish encoding");
|
|
668
|
+
}
|
|
669
|
+
this.finished = true;
|
|
670
|
+
const outputPtr = this.module._sip_encoder_get_output(this.encoder);
|
|
671
|
+
const outputSize = this.module._sip_encoder_get_output_size(this.encoder);
|
|
672
|
+
if (!outputPtr || !outputSize) {
|
|
673
|
+
throw new Error("No output data");
|
|
674
|
+
}
|
|
675
|
+
const output = copyFromWasm(this.module, outputPtr, outputSize);
|
|
676
|
+
return output.buffer;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Encode a full RGB buffer to JPEG
|
|
680
|
+
*
|
|
681
|
+
* @param pixels - RGB pixel data (width * height * 3 bytes)
|
|
682
|
+
* @returns JPEG data as ArrayBuffer
|
|
683
|
+
*/
|
|
684
|
+
encodeAll(pixels) {
|
|
685
|
+
if (pixels.length !== this.width * this.height * 3) {
|
|
686
|
+
throw new Error(`Invalid pixel data size: expected ${this.width * this.height * 3}, got ${pixels.length}`);
|
|
687
|
+
}
|
|
688
|
+
this.start();
|
|
689
|
+
const rowSize = this.width * 3;
|
|
690
|
+
for (let y = 0; y < this.height; y++) {
|
|
691
|
+
const rowData = pixels.subarray(y * rowSize, (y + 1) * rowSize);
|
|
692
|
+
this.writeScanlineData(rowData);
|
|
693
|
+
}
|
|
694
|
+
return this.finish();
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Clean up resources
|
|
698
|
+
*/
|
|
699
|
+
dispose() {
|
|
700
|
+
if (this.encoder) {
|
|
701
|
+
this.module._sip_encoder_destroy(this.encoder);
|
|
702
|
+
this.encoder = 0;
|
|
703
|
+
}
|
|
704
|
+
this.started = false;
|
|
705
|
+
this.finished = false;
|
|
706
|
+
this.rowBufferPtr = 0;
|
|
707
|
+
this.currentLine = 0;
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/wasm/png-decoder.ts
|
|
712
|
+
var WasmPngDecoder = class {
|
|
713
|
+
module;
|
|
714
|
+
decoder = 0;
|
|
715
|
+
dataPtr = 0;
|
|
716
|
+
width = 0;
|
|
717
|
+
height = 0;
|
|
718
|
+
hasAlpha = false;
|
|
719
|
+
rowBufferPtr = 0;
|
|
720
|
+
started = false;
|
|
721
|
+
finished = false;
|
|
722
|
+
currentRow = 0;
|
|
723
|
+
constructor() {
|
|
724
|
+
this.module = getWasmModule();
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Initialize decoder with PNG data
|
|
728
|
+
*/
|
|
729
|
+
init(data) {
|
|
730
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
731
|
+
this.decoder = this.module._sip_png_decoder_create();
|
|
732
|
+
if (!this.decoder) {
|
|
733
|
+
throw new Error("Failed to create PNG decoder");
|
|
734
|
+
}
|
|
735
|
+
this.dataPtr = copyToWasm(this.module, bytes);
|
|
736
|
+
if (this.module._sip_png_decoder_set_source(this.decoder, this.dataPtr, bytes.length) !== 0) {
|
|
737
|
+
this.dispose();
|
|
738
|
+
throw new Error("Failed to set PNG decoder source");
|
|
739
|
+
}
|
|
740
|
+
if (this.module._sip_png_decoder_read_header(this.decoder) !== 0) {
|
|
741
|
+
this.dispose();
|
|
742
|
+
throw new Error("Failed to read PNG header");
|
|
743
|
+
}
|
|
744
|
+
this.width = this.module._sip_png_decoder_get_width(this.decoder);
|
|
745
|
+
this.height = this.module._sip_png_decoder_get_height(this.decoder);
|
|
746
|
+
this.hasAlpha = this.module._sip_png_decoder_has_alpha(this.decoder) !== 0;
|
|
747
|
+
return { width: this.width, height: this.height, hasAlpha: this.hasAlpha };
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Get image dimensions
|
|
751
|
+
*/
|
|
752
|
+
getDimensions() {
|
|
753
|
+
return { width: this.width, height: this.height };
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Check if image has alpha channel
|
|
757
|
+
*/
|
|
758
|
+
getHasAlpha() {
|
|
759
|
+
return this.hasAlpha;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Start decoding
|
|
763
|
+
*/
|
|
764
|
+
start() {
|
|
765
|
+
if (!this.decoder) {
|
|
766
|
+
throw new Error("Decoder not initialized");
|
|
767
|
+
}
|
|
768
|
+
if (this.started) {
|
|
769
|
+
throw new Error("Decoding already started");
|
|
770
|
+
}
|
|
771
|
+
if (this.module._sip_png_decoder_start(this.decoder) !== 0) {
|
|
772
|
+
throw new Error("Failed to start PNG decompression");
|
|
773
|
+
}
|
|
774
|
+
this.rowBufferPtr = this.module._sip_png_decoder_get_row_buffer(this.decoder);
|
|
775
|
+
if (!this.rowBufferPtr) {
|
|
776
|
+
throw new Error("Failed to get row buffer");
|
|
777
|
+
}
|
|
778
|
+
this.started = true;
|
|
779
|
+
this.currentRow = 0;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Read next scanline
|
|
783
|
+
*
|
|
784
|
+
* @returns Scanline object or null if no more scanlines
|
|
785
|
+
*/
|
|
786
|
+
readScanline() {
|
|
787
|
+
if (!this.started || this.finished) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
if (this.currentRow >= this.height) {
|
|
791
|
+
this.finished = true;
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
const result = this.module._sip_png_decoder_read_row(this.decoder);
|
|
795
|
+
if (result < 0) {
|
|
796
|
+
throw new Error("Failed to read PNG row");
|
|
797
|
+
}
|
|
798
|
+
const rowSize = this.width * 3;
|
|
799
|
+
const data = new Uint8Array(
|
|
800
|
+
this.module.HEAPU8.buffer,
|
|
801
|
+
this.rowBufferPtr,
|
|
802
|
+
rowSize
|
|
803
|
+
).slice();
|
|
804
|
+
const y = this.currentRow;
|
|
805
|
+
this.currentRow++;
|
|
806
|
+
if (result === 0 || this.currentRow >= this.height) {
|
|
807
|
+
this.finished = true;
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
data,
|
|
811
|
+
width: this.width,
|
|
812
|
+
y
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Read all remaining scanlines
|
|
817
|
+
*
|
|
818
|
+
* @yields Scanline objects
|
|
819
|
+
*/
|
|
820
|
+
*readAllScanlines() {
|
|
821
|
+
let scanline;
|
|
822
|
+
while ((scanline = this.readScanline()) !== null) {
|
|
823
|
+
yield scanline;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Decode entire image to RGB buffer
|
|
828
|
+
*
|
|
829
|
+
* @returns Full RGB pixel buffer
|
|
830
|
+
*/
|
|
831
|
+
decodeAll() {
|
|
832
|
+
if (!this.started) {
|
|
833
|
+
this.start();
|
|
834
|
+
}
|
|
835
|
+
const pixels = new Uint8Array(this.width * this.height * 3);
|
|
836
|
+
const rowSize = this.width * 3;
|
|
837
|
+
for (const scanline of this.readAllScanlines()) {
|
|
838
|
+
pixels.set(scanline.data, scanline.y * rowSize);
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
pixels,
|
|
842
|
+
width: this.width,
|
|
843
|
+
height: this.height
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Clean up resources
|
|
848
|
+
*/
|
|
849
|
+
dispose() {
|
|
850
|
+
if (this.decoder) {
|
|
851
|
+
this.module._sip_png_decoder_destroy(this.decoder);
|
|
852
|
+
this.decoder = 0;
|
|
853
|
+
}
|
|
854
|
+
if (this.dataPtr) {
|
|
855
|
+
this.module._free(this.dataPtr);
|
|
856
|
+
this.dataPtr = 0;
|
|
857
|
+
}
|
|
858
|
+
this.started = false;
|
|
859
|
+
this.finished = false;
|
|
860
|
+
this.rowBufferPtr = 0;
|
|
861
|
+
this.currentRow = 0;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// src/encoder.ts
|
|
866
|
+
var NativeEncoder = class {
|
|
867
|
+
supportsScanline = true;
|
|
868
|
+
width = 0;
|
|
869
|
+
height = 0;
|
|
870
|
+
quality = 85;
|
|
871
|
+
wasmEncoder = null;
|
|
872
|
+
async init(width, height, quality) {
|
|
873
|
+
this.width = width;
|
|
874
|
+
this.height = height;
|
|
875
|
+
this.quality = quality;
|
|
876
|
+
await loadWasm();
|
|
877
|
+
this.wasmEncoder = new WasmJpegEncoder();
|
|
878
|
+
this.wasmEncoder.init(width, height, quality);
|
|
879
|
+
}
|
|
880
|
+
async encode(pixels) {
|
|
881
|
+
if (!this.wasmEncoder) {
|
|
882
|
+
throw new Error("Encoder not initialized. Call init() first.");
|
|
883
|
+
}
|
|
884
|
+
return this.wasmEncoder.encodeAll(pixels);
|
|
885
|
+
}
|
|
886
|
+
dispose() {
|
|
887
|
+
if (this.wasmEncoder) {
|
|
888
|
+
this.wasmEncoder.dispose();
|
|
889
|
+
this.wasmEncoder = null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
async function createEncoder(width, height, quality) {
|
|
894
|
+
const encoder = new NativeEncoder();
|
|
895
|
+
await encoder.init(width, height, quality);
|
|
896
|
+
return encoder;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/resize.ts
|
|
900
|
+
function createResizeState(srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
901
|
+
return {
|
|
902
|
+
srcWidth,
|
|
903
|
+
srcHeight,
|
|
904
|
+
dstWidth,
|
|
905
|
+
dstHeight,
|
|
906
|
+
bufferA: null,
|
|
907
|
+
bufferB: null,
|
|
908
|
+
bufferAY: -1,
|
|
909
|
+
bufferBY: -1,
|
|
910
|
+
currentOutputY: 0
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function resizeRowHorizontal(src, srcWidth, dstWidth) {
|
|
914
|
+
const dst = new Uint8Array(dstWidth * 3);
|
|
915
|
+
const xScale = srcWidth / dstWidth;
|
|
916
|
+
for (let dstX = 0; dstX < dstWidth; dstX++) {
|
|
917
|
+
const srcXFloat = dstX * xScale;
|
|
918
|
+
const srcX0 = Math.floor(srcXFloat);
|
|
919
|
+
const srcX1 = Math.min(srcX0 + 1, srcWidth - 1);
|
|
920
|
+
const t = srcXFloat - srcX0;
|
|
921
|
+
const invT = 1 - t;
|
|
922
|
+
const src0 = srcX0 * 3;
|
|
923
|
+
const src1 = srcX1 * 3;
|
|
924
|
+
const dstOffset = dstX * 3;
|
|
925
|
+
dst[dstOffset] = Math.round(src[src0] * invT + src[src1] * t);
|
|
926
|
+
dst[dstOffset + 1] = Math.round(src[src0 + 1] * invT + src[src1 + 1] * t);
|
|
927
|
+
dst[dstOffset + 2] = Math.round(src[src0 + 2] * invT + src[src1 + 2] * t);
|
|
928
|
+
}
|
|
929
|
+
return dst;
|
|
930
|
+
}
|
|
931
|
+
function blendRows(rowA, rowB, t, width) {
|
|
932
|
+
const result = new Uint8Array(width * 3);
|
|
933
|
+
const invT = 1 - t;
|
|
934
|
+
for (let i = 0; i < width * 3; i++) {
|
|
935
|
+
result[i] = Math.round(rowA[i] * invT + rowB[i] * t);
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
function processScanline(state, srcScanline, srcY) {
|
|
940
|
+
const { srcWidth, srcHeight, dstWidth, dstHeight } = state;
|
|
941
|
+
const yScale = srcHeight / dstHeight;
|
|
942
|
+
const output = [];
|
|
943
|
+
const resizedRow = resizeRowHorizontal(srcScanline, srcWidth, dstWidth);
|
|
944
|
+
state.bufferA = state.bufferB;
|
|
945
|
+
state.bufferAY = state.bufferBY;
|
|
946
|
+
state.bufferB = resizedRow;
|
|
947
|
+
state.bufferBY = srcY;
|
|
948
|
+
while (state.currentOutputY < dstHeight) {
|
|
949
|
+
const srcYFloat = state.currentOutputY * yScale;
|
|
950
|
+
const srcYFloor = Math.floor(srcYFloat);
|
|
951
|
+
const srcYCeil = Math.min(srcYFloor + 1, srcHeight - 1);
|
|
952
|
+
if (srcYCeil > srcY) {
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
if (state.bufferA === null) {
|
|
956
|
+
output.push({
|
|
957
|
+
data: state.bufferB,
|
|
958
|
+
width: dstWidth,
|
|
959
|
+
y: state.currentOutputY
|
|
960
|
+
});
|
|
961
|
+
state.currentOutputY++;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const t = srcYFloat - srcYFloor;
|
|
965
|
+
let rowA = state.bufferA;
|
|
966
|
+
let rowB = state.bufferB;
|
|
967
|
+
if (srcYFloor === state.bufferBY) {
|
|
968
|
+
rowA = state.bufferB;
|
|
969
|
+
rowB = state.bufferB;
|
|
970
|
+
} else if (srcYCeil === state.bufferAY) {
|
|
971
|
+
rowA = state.bufferA;
|
|
972
|
+
rowB = state.bufferA;
|
|
973
|
+
}
|
|
974
|
+
const blended = blendRows(rowA, rowB, t, dstWidth);
|
|
975
|
+
output.push({
|
|
976
|
+
data: blended,
|
|
977
|
+
width: dstWidth,
|
|
978
|
+
y: state.currentOutputY
|
|
979
|
+
});
|
|
980
|
+
state.currentOutputY++;
|
|
981
|
+
}
|
|
982
|
+
return output;
|
|
983
|
+
}
|
|
984
|
+
function flushResize(state) {
|
|
985
|
+
const output = [];
|
|
986
|
+
while (state.currentOutputY < state.dstHeight) {
|
|
987
|
+
if (state.bufferB === null) break;
|
|
988
|
+
output.push({
|
|
989
|
+
data: state.bufferB,
|
|
990
|
+
width: state.dstWidth,
|
|
991
|
+
y: state.currentOutputY
|
|
992
|
+
});
|
|
993
|
+
state.currentOutputY++;
|
|
994
|
+
}
|
|
995
|
+
return output;
|
|
996
|
+
}
|
|
997
|
+
function calculateTargetDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
|
|
998
|
+
const scaleX = maxWidth / srcWidth;
|
|
999
|
+
const scaleY = maxHeight / srcHeight;
|
|
1000
|
+
const scale = Math.min(scaleX, scaleY, 1);
|
|
1001
|
+
return {
|
|
1002
|
+
width: Math.round(srcWidth * scale),
|
|
1003
|
+
height: Math.round(srcHeight * scale),
|
|
1004
|
+
scale
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function calculateDctScaleFactor(srcWidth, srcHeight, targetWidth, targetHeight) {
|
|
1008
|
+
const scales = [8, 4, 2, 1];
|
|
1009
|
+
for (const scale of scales) {
|
|
1010
|
+
const scaledWidth = Math.ceil(srcWidth / scale);
|
|
1011
|
+
const scaledHeight = Math.ceil(srcHeight / scale);
|
|
1012
|
+
if (scaledWidth >= targetWidth && scaledHeight >= targetHeight) {
|
|
1013
|
+
return scale;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return 1;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/streaming.ts
|
|
1020
|
+
var DEFAULT_OPTIONS = {
|
|
1021
|
+
maxWidth: 4096,
|
|
1022
|
+
maxHeight: 4096,
|
|
1023
|
+
maxBytes: 1.5 * 1024 * 1024,
|
|
1024
|
+
quality: 85
|
|
1025
|
+
};
|
|
1026
|
+
async function processJpegStreaming(input, options = {}) {
|
|
1027
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1028
|
+
await loadWasm();
|
|
1029
|
+
const decoder = new WasmJpegDecoder();
|
|
1030
|
+
try {
|
|
1031
|
+
const { width: srcWidth, height: srcHeight } = decoder.init(input);
|
|
1032
|
+
const target = calculateTargetDimensions(
|
|
1033
|
+
srcWidth,
|
|
1034
|
+
srcHeight,
|
|
1035
|
+
opts.maxWidth,
|
|
1036
|
+
opts.maxHeight
|
|
1037
|
+
);
|
|
1038
|
+
const dctScale = calculateOptimalScale(
|
|
1039
|
+
srcWidth,
|
|
1040
|
+
srcHeight,
|
|
1041
|
+
target.width,
|
|
1042
|
+
target.height
|
|
1043
|
+
);
|
|
1044
|
+
const { width: decodeWidth, height: decodeHeight } = decoder.setScale(dctScale);
|
|
1045
|
+
const resizeState = createResizeState(
|
|
1046
|
+
decodeWidth,
|
|
1047
|
+
decodeHeight,
|
|
1048
|
+
target.width,
|
|
1049
|
+
target.height
|
|
1050
|
+
);
|
|
1051
|
+
const encoder = new WasmJpegEncoder();
|
|
1052
|
+
encoder.init(target.width, target.height, opts.quality);
|
|
1053
|
+
encoder.start();
|
|
1054
|
+
decoder.start();
|
|
1055
|
+
let decodedLine = 0;
|
|
1056
|
+
for (const scanline of decoder.readAllScanlines()) {
|
|
1057
|
+
const outputScanlines = processScanline(resizeState, scanline.data, decodedLine);
|
|
1058
|
+
decodedLine++;
|
|
1059
|
+
for (const outScanline of outputScanlines) {
|
|
1060
|
+
encoder.writeScanline(outScanline);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const remaining = flushResize(resizeState);
|
|
1064
|
+
for (const outScanline of remaining) {
|
|
1065
|
+
encoder.writeScanline(outScanline);
|
|
1066
|
+
}
|
|
1067
|
+
const jpegData = encoder.finish();
|
|
1068
|
+
if (jpegData.byteLength > opts.maxBytes && opts.quality > 45) {
|
|
1069
|
+
encoder.dispose();
|
|
1070
|
+
decoder.dispose();
|
|
1071
|
+
return processJpegStreaming(input, {
|
|
1072
|
+
...opts,
|
|
1073
|
+
quality: opts.quality - 10
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
encoder.dispose();
|
|
1077
|
+
return {
|
|
1078
|
+
data: jpegData,
|
|
1079
|
+
width: target.width,
|
|
1080
|
+
height: target.height,
|
|
1081
|
+
mimeType: "image/jpeg",
|
|
1082
|
+
originalFormat: "jpeg"
|
|
1083
|
+
};
|
|
1084
|
+
} finally {
|
|
1085
|
+
decoder.dispose();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async function processPngStreaming(input, options = {}) {
|
|
1089
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1090
|
+
await loadWasm();
|
|
1091
|
+
const decoder = new WasmPngDecoder();
|
|
1092
|
+
try {
|
|
1093
|
+
const { width: srcWidth, height: srcHeight } = decoder.init(input);
|
|
1094
|
+
const target = calculateTargetDimensions(
|
|
1095
|
+
srcWidth,
|
|
1096
|
+
srcHeight,
|
|
1097
|
+
opts.maxWidth,
|
|
1098
|
+
opts.maxHeight
|
|
1099
|
+
);
|
|
1100
|
+
const resizeState = createResizeState(
|
|
1101
|
+
srcWidth,
|
|
1102
|
+
srcHeight,
|
|
1103
|
+
target.width,
|
|
1104
|
+
target.height
|
|
1105
|
+
);
|
|
1106
|
+
const encoder = new WasmJpegEncoder();
|
|
1107
|
+
encoder.init(target.width, target.height, opts.quality);
|
|
1108
|
+
encoder.start();
|
|
1109
|
+
decoder.start();
|
|
1110
|
+
let decodedLine = 0;
|
|
1111
|
+
for (const scanline of decoder.readAllScanlines()) {
|
|
1112
|
+
const outputScanlines = processScanline(resizeState, scanline.data, decodedLine);
|
|
1113
|
+
decodedLine++;
|
|
1114
|
+
for (const outScanline of outputScanlines) {
|
|
1115
|
+
encoder.writeScanline(outScanline);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const remaining = flushResize(resizeState);
|
|
1119
|
+
for (const outScanline of remaining) {
|
|
1120
|
+
encoder.writeScanline(outScanline);
|
|
1121
|
+
}
|
|
1122
|
+
const jpegData = encoder.finish();
|
|
1123
|
+
if (jpegData.byteLength > opts.maxBytes && opts.quality > 45) {
|
|
1124
|
+
encoder.dispose();
|
|
1125
|
+
decoder.dispose();
|
|
1126
|
+
return processPngStreaming(input, {
|
|
1127
|
+
...opts,
|
|
1128
|
+
quality: opts.quality - 10
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
encoder.dispose();
|
|
1132
|
+
return {
|
|
1133
|
+
data: jpegData,
|
|
1134
|
+
width: target.width,
|
|
1135
|
+
height: target.height,
|
|
1136
|
+
mimeType: "image/jpeg",
|
|
1137
|
+
originalFormat: "png"
|
|
1138
|
+
};
|
|
1139
|
+
} finally {
|
|
1140
|
+
decoder.dispose();
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function isStreamingAvailable() {
|
|
1144
|
+
return isWasmAvailable();
|
|
1145
|
+
}
|
|
1146
|
+
async function initStreaming() {
|
|
1147
|
+
try {
|
|
1148
|
+
await loadWasm();
|
|
1149
|
+
return true;
|
|
1150
|
+
} catch {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/pipeline.ts
|
|
1156
|
+
var DEFAULT_OPTIONS2 = {
|
|
1157
|
+
maxWidth: 4096,
|
|
1158
|
+
maxHeight: 4096,
|
|
1159
|
+
maxBytes: 1.5 * 1024 * 1024,
|
|
1160
|
+
// 1.5MB
|
|
1161
|
+
quality: 85
|
|
1162
|
+
};
|
|
1163
|
+
async function process2(input, options = {}) {
|
|
1164
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
1165
|
+
const probeResult = probe(input);
|
|
1166
|
+
if (probeResult.format === "unknown") {
|
|
1167
|
+
throw new Error("Unknown image format");
|
|
1168
|
+
}
|
|
1169
|
+
const { format, width: srcWidth, height: srcHeight } = probeResult;
|
|
1170
|
+
if (format === "jpeg") {
|
|
1171
|
+
return await processJpegStreaming(input, opts);
|
|
1172
|
+
}
|
|
1173
|
+
if (format === "png") {
|
|
1174
|
+
return await processPngStreaming(input, opts);
|
|
1175
|
+
}
|
|
1176
|
+
await loadWasm();
|
|
1177
|
+
const target = calculateTargetDimensions(
|
|
1178
|
+
srcWidth,
|
|
1179
|
+
srcHeight,
|
|
1180
|
+
opts.maxWidth,
|
|
1181
|
+
opts.maxHeight
|
|
1182
|
+
);
|
|
1183
|
+
const decoder = await createDecoder(format, input);
|
|
1184
|
+
const { pixels: srcPixels, width: decodedWidth, height: decodedHeight } = await decoder.decode();
|
|
1185
|
+
decoder.dispose();
|
|
1186
|
+
const resizedPixels = resizePixelBuffer(
|
|
1187
|
+
srcPixels,
|
|
1188
|
+
decodedWidth,
|
|
1189
|
+
decodedHeight,
|
|
1190
|
+
target.width,
|
|
1191
|
+
target.height
|
|
1192
|
+
);
|
|
1193
|
+
let quality = opts.quality;
|
|
1194
|
+
let jpegData = await encodeToJpeg(resizedPixels, target.width, target.height, quality);
|
|
1195
|
+
while (jpegData.byteLength > opts.maxBytes && quality > 45) {
|
|
1196
|
+
quality -= 10;
|
|
1197
|
+
jpegData = await encodeToJpeg(resizedPixels, target.width, target.height, quality);
|
|
1198
|
+
}
|
|
1199
|
+
if (jpegData.byteLength > opts.maxBytes) {
|
|
1200
|
+
const scaleFactor = Math.sqrt(opts.maxBytes / jpegData.byteLength) * 0.9;
|
|
1201
|
+
const newWidth = Math.round(target.width * scaleFactor);
|
|
1202
|
+
const newHeight = Math.round(target.height * scaleFactor);
|
|
1203
|
+
const smallerPixels = resizePixelBuffer(
|
|
1204
|
+
resizedPixels,
|
|
1205
|
+
target.width,
|
|
1206
|
+
target.height,
|
|
1207
|
+
newWidth,
|
|
1208
|
+
newHeight
|
|
1209
|
+
);
|
|
1210
|
+
jpegData = await encodeToJpeg(smallerPixels, newWidth, newHeight, quality);
|
|
1211
|
+
return {
|
|
1212
|
+
data: jpegData,
|
|
1213
|
+
width: newWidth,
|
|
1214
|
+
height: newHeight,
|
|
1215
|
+
mimeType: "image/jpeg",
|
|
1216
|
+
originalFormat: format
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
data: jpegData,
|
|
1221
|
+
width: target.width,
|
|
1222
|
+
height: target.height,
|
|
1223
|
+
mimeType: "image/jpeg",
|
|
1224
|
+
originalFormat: format
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function resizePixelBuffer(srcPixels, srcWidth, srcHeight, dstWidth, dstHeight) {
|
|
1228
|
+
if (srcWidth === dstWidth && srcHeight === dstHeight) {
|
|
1229
|
+
return srcPixels;
|
|
1230
|
+
}
|
|
1231
|
+
const state = createResizeState(srcWidth, srcHeight, dstWidth, dstHeight);
|
|
1232
|
+
const outputRows = new Array(dstHeight);
|
|
1233
|
+
const srcRowSize = srcWidth * 3;
|
|
1234
|
+
for (let y = 0; y < srcHeight; y++) {
|
|
1235
|
+
const srcRow = srcPixels.subarray(y * srcRowSize, (y + 1) * srcRowSize);
|
|
1236
|
+
const outputScanlines = processScanline(state, srcRow, y);
|
|
1237
|
+
for (const scanline of outputScanlines) {
|
|
1238
|
+
outputRows[scanline.y] = scanline.data;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const remaining = flushResize(state);
|
|
1242
|
+
for (const scanline of remaining) {
|
|
1243
|
+
outputRows[scanline.y] = scanline.data;
|
|
1244
|
+
}
|
|
1245
|
+
const dstRowSize = dstWidth * 3;
|
|
1246
|
+
const result = new Uint8Array(dstWidth * dstHeight * 3);
|
|
1247
|
+
for (let y = 0; y < dstHeight; y++) {
|
|
1248
|
+
if (outputRows[y]) {
|
|
1249
|
+
result.set(outputRows[y], y * dstRowSize);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return result;
|
|
1253
|
+
}
|
|
1254
|
+
async function encodeToJpeg(pixels, width, height, quality) {
|
|
1255
|
+
const encoder = await createEncoder(width, height, quality);
|
|
1256
|
+
const result = await encoder.encode(pixels);
|
|
1257
|
+
encoder.dispose();
|
|
1258
|
+
return result;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/index.ts
|
|
1262
|
+
var sip = {
|
|
1263
|
+
process: process2,
|
|
1264
|
+
probe,
|
|
1265
|
+
detectImageFormat,
|
|
1266
|
+
initStreaming,
|
|
1267
|
+
isStreamingAvailable
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
export { WasmJpegDecoder, WasmJpegEncoder, WasmPngDecoder, calculateDctScaleFactor, calculateOptimalScale, calculateTargetDimensions, createResizeState, detectImageFormat, flushResize, getWasmModule, initStreaming, initWithWasmModule, isStreamingAvailable, isWasmAvailable, loadWasm, probe, process2 as process, processJpegStreaming, processScanline, sip };
|
|
1271
|
+
//# sourceMappingURL=index.js.map
|
|
1272
|
+
//# sourceMappingURL=index.js.map
|