cross-image 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +292 -74
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +17 -5
- package/esm/src/formats/apng.js +104 -9
- package/esm/src/formats/ascii.d.ts +13 -3
- package/esm/src/formats/ascii.js +25 -1
- package/esm/src/formats/avif.d.ts +96 -0
- package/esm/src/formats/avif.js +607 -0
- package/esm/src/formats/bmp.d.ts +13 -3
- package/esm/src/formats/bmp.js +75 -2
- package/esm/src/formats/dng.d.ts +14 -2
- package/esm/src/formats/dng.js +27 -5
- package/esm/src/formats/gif.d.ts +18 -5
- package/esm/src/formats/gif.js +160 -14
- package/esm/src/formats/heic.d.ts +96 -0
- package/esm/src/formats/heic.js +608 -0
- package/esm/src/formats/ico.d.ts +13 -3
- package/esm/src/formats/ico.js +32 -4
- package/esm/src/formats/jpeg.d.ts +10 -3
- package/esm/src/formats/jpeg.js +99 -11
- package/esm/src/formats/pam.d.ts +13 -3
- package/esm/src/formats/pam.js +68 -2
- package/esm/src/formats/pcx.d.ts +13 -3
- package/esm/src/formats/pcx.js +47 -2
- package/esm/src/formats/png.d.ts +15 -3
- package/esm/src/formats/png.js +89 -2
- package/esm/src/formats/png_base.js +2 -5
- package/esm/src/formats/ppm.d.ts +13 -3
- package/esm/src/formats/ppm.js +36 -2
- package/esm/src/formats/tiff.d.ts +14 -18
- package/esm/src/formats/tiff.js +219 -20
- package/esm/src/formats/webp.d.ts +10 -3
- package/esm/src/formats/webp.js +103 -8
- package/esm/src/image.d.ts +20 -3
- package/esm/src/image.js +65 -21
- package/esm/src/types.d.ts +74 -4
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- package/esm/src/utils/image_processing.js +144 -70
- package/esm/src/utils/jpeg_decoder.d.ts +17 -4
- package/esm/src/utils/jpeg_decoder.js +448 -83
- package/esm/src/utils/jpeg_encoder.d.ts +15 -1
- package/esm/src/utils/jpeg_encoder.js +263 -24
- package/esm/src/utils/resize.js +51 -20
- package/esm/src/utils/tiff_deflate.d.ts +18 -0
- package/esm/src/utils/tiff_deflate.js +27 -0
- package/esm/src/utils/tiff_packbits.d.ts +24 -0
- package/esm/src/utils/tiff_packbits.js +90 -0
- package/esm/src/utils/webp_decoder.d.ts +3 -1
- package/esm/src/utils/webp_decoder.js +144 -63
- package/esm/src/utils/webp_encoder.js +5 -11
- package/package.json +18 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +7 -3
- package/script/src/formats/apng.d.ts +17 -5
- package/script/src/formats/apng.js +104 -9
- package/script/src/formats/ascii.d.ts +13 -3
- package/script/src/formats/ascii.js +25 -1
- package/script/src/formats/avif.d.ts +96 -0
- package/script/src/formats/avif.js +611 -0
- package/script/src/formats/bmp.d.ts +13 -3
- package/script/src/formats/bmp.js +75 -2
- package/script/src/formats/dng.d.ts +14 -2
- package/script/src/formats/dng.js +27 -5
- package/script/src/formats/gif.d.ts +18 -5
- package/script/src/formats/gif.js +160 -14
- package/script/src/formats/heic.d.ts +96 -0
- package/script/src/formats/heic.js +612 -0
- package/script/src/formats/ico.d.ts +13 -3
- package/script/src/formats/ico.js +32 -4
- package/script/src/formats/jpeg.d.ts +10 -3
- package/script/src/formats/jpeg.js +99 -11
- package/script/src/formats/pam.d.ts +13 -3
- package/script/src/formats/pam.js +68 -2
- package/script/src/formats/pcx.d.ts +13 -3
- package/script/src/formats/pcx.js +47 -2
- package/script/src/formats/png.d.ts +15 -3
- package/script/src/formats/png.js +89 -2
- package/script/src/formats/png_base.js +2 -5
- package/script/src/formats/ppm.d.ts +13 -3
- package/script/src/formats/ppm.js +36 -2
- package/script/src/formats/tiff.d.ts +14 -18
- package/script/src/formats/tiff.js +219 -20
- package/script/src/formats/webp.d.ts +10 -3
- package/script/src/formats/webp.js +103 -8
- package/script/src/image.d.ts +20 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +74 -4
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- package/script/src/utils/image_processing.js +144 -70
- package/script/src/utils/jpeg_decoder.d.ts +17 -4
- package/script/src/utils/jpeg_decoder.js +448 -83
- package/script/src/utils/jpeg_encoder.d.ts +15 -1
- package/script/src/utils/jpeg_encoder.js +263 -24
- package/script/src/utils/resize.js +51 -20
- package/script/src/utils/tiff_deflate.d.ts +18 -0
- package/script/src/utils/tiff_deflate.js +31 -0
- package/script/src/utils/tiff_packbits.d.ts +24 -0
- package/script/src/utils/tiff_packbits.js +94 -0
- package/script/src/utils/webp_decoder.d.ts +3 -1
- package/script/src/utils/webp_decoder.js +144 -63
- package/script/src/utils/webp_encoder.js +5 -11
|
@@ -7,7 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.GIFDecoder = void 0;
|
|
8
8
|
const lzw_js_1 = require("./lzw.js");
|
|
9
9
|
class GIFDecoder {
|
|
10
|
-
constructor(data) {
|
|
10
|
+
constructor(data, settings = {}) {
|
|
11
11
|
Object.defineProperty(this, "data", {
|
|
12
12
|
enumerable: true,
|
|
13
13
|
configurable: true,
|
|
@@ -20,8 +20,18 @@ class GIFDecoder {
|
|
|
20
20
|
writable: true,
|
|
21
21
|
value: void 0
|
|
22
22
|
});
|
|
23
|
+
Object.defineProperty(this, "options", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
23
29
|
this.data = data;
|
|
24
30
|
this.pos = 0;
|
|
31
|
+
this.options = {
|
|
32
|
+
tolerantDecoding: settings.tolerantDecoding ?? true,
|
|
33
|
+
onWarning: settings.onWarning,
|
|
34
|
+
};
|
|
25
35
|
}
|
|
26
36
|
readByte() {
|
|
27
37
|
if (this.pos >= this.data.length) {
|
|
@@ -210,72 +220,20 @@ class GIFDecoder {
|
|
|
210
220
|
}
|
|
211
221
|
else if (separator === 0x2c) {
|
|
212
222
|
// Image Descriptor
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const localColorTableSize = 2 << (packed & 0x07);
|
|
222
|
-
let localColorTable = null;
|
|
223
|
-
if (hasLocalColorTable) {
|
|
224
|
-
localColorTable = this.readColorTable(localColorTableSize);
|
|
225
|
-
}
|
|
226
|
-
// Read image data
|
|
227
|
-
const minCodeSize = this.readByte();
|
|
228
|
-
const compressedData = this.readDataSubBlocks();
|
|
229
|
-
// Decompress using LZW
|
|
230
|
-
const decoder = new lzw_js_1.LZWDecoder(minCodeSize, compressedData);
|
|
231
|
-
const indexedData = decoder.decompress();
|
|
232
|
-
// Convert indexed to RGBA
|
|
233
|
-
const colorTable = localColorTable || globalColorTable;
|
|
234
|
-
if (!colorTable) {
|
|
235
|
-
throw new Error("No color table available");
|
|
236
|
-
}
|
|
237
|
-
// Deinterlace if necessary
|
|
238
|
-
const deinterlaced = interlaced
|
|
239
|
-
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
240
|
-
: indexedData;
|
|
241
|
-
// Create frame with just the image data (not full canvas)
|
|
242
|
-
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
243
|
-
for (let y = 0; y < imageHeight; y++) {
|
|
244
|
-
for (let x = 0; x < imageWidth; x++) {
|
|
245
|
-
const srcIdx = y * imageWidth + x;
|
|
246
|
-
if (srcIdx >= deinterlaced.length)
|
|
247
|
-
continue;
|
|
248
|
-
const colorIndex = deinterlaced[srcIdx];
|
|
249
|
-
const dstIdx = (y * imageWidth + x) * 4;
|
|
250
|
-
if (transparentColorIndex !== null &&
|
|
251
|
-
colorIndex === transparentColorIndex) {
|
|
252
|
-
// Transparent pixel
|
|
253
|
-
frameData[dstIdx] = 0;
|
|
254
|
-
frameData[dstIdx + 1] = 0;
|
|
255
|
-
frameData[dstIdx + 2] = 0;
|
|
256
|
-
frameData[dstIdx + 3] = 0;
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
// Copy color from color table
|
|
260
|
-
const colorOffset = colorIndex * 3;
|
|
261
|
-
if (colorOffset + 2 < colorTable.length) {
|
|
262
|
-
frameData[dstIdx] = colorTable[colorOffset];
|
|
263
|
-
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
264
|
-
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
265
|
-
frameData[dstIdx + 3] = 255;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
223
|
+
if (this.options.tolerantDecoding) {
|
|
224
|
+
try {
|
|
225
|
+
this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
// Tolerant decoding: skip corrupted frames and continue
|
|
229
|
+
// This allows partial decoding of multi-frame GIFs with some bad frames
|
|
230
|
+
this.options.onWarning?.("GIF: Skipping corrupted frame", e);
|
|
268
231
|
}
|
|
269
232
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
top: imageTop,
|
|
275
|
-
data: frameData,
|
|
276
|
-
delay: delayTime,
|
|
277
|
-
disposal: disposalMethod,
|
|
278
|
-
});
|
|
233
|
+
else {
|
|
234
|
+
// Non-tolerant mode: throw on first error
|
|
235
|
+
this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
|
|
236
|
+
}
|
|
279
237
|
// Reset graphic control extension state
|
|
280
238
|
transparentColorIndex = null;
|
|
281
239
|
delayTime = 0;
|
|
@@ -353,6 +311,74 @@ class GIFDecoder {
|
|
|
353
311
|
data: rgba,
|
|
354
312
|
};
|
|
355
313
|
}
|
|
314
|
+
decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod) {
|
|
315
|
+
const imageLeft = this.readUint16LE();
|
|
316
|
+
const imageTop = this.readUint16LE();
|
|
317
|
+
const imageWidth = this.readUint16LE();
|
|
318
|
+
const imageHeight = this.readUint16LE();
|
|
319
|
+
const packed = this.readByte();
|
|
320
|
+
const hasLocalColorTable = (packed & 0x80) !== 0;
|
|
321
|
+
const interlaced = (packed & 0x40) !== 0;
|
|
322
|
+
// Color table size: 2^(n+1) where n is the 3 least significant bits
|
|
323
|
+
const localColorTableSize = 2 << (packed & 0x07);
|
|
324
|
+
let localColorTable = null;
|
|
325
|
+
if (hasLocalColorTable) {
|
|
326
|
+
localColorTable = this.readColorTable(localColorTableSize);
|
|
327
|
+
}
|
|
328
|
+
// Read image data
|
|
329
|
+
const minCodeSize = this.readByte();
|
|
330
|
+
const compressedData = this.readDataSubBlocks();
|
|
331
|
+
// Decompress using LZW
|
|
332
|
+
const decoder = new lzw_js_1.LZWDecoder(minCodeSize, compressedData);
|
|
333
|
+
const indexedData = decoder.decompress();
|
|
334
|
+
// Convert indexed to RGBA
|
|
335
|
+
const colorTable = localColorTable || globalColorTable;
|
|
336
|
+
if (!colorTable) {
|
|
337
|
+
throw new Error("No color table available");
|
|
338
|
+
}
|
|
339
|
+
// Deinterlace if necessary
|
|
340
|
+
const deinterlaced = interlaced
|
|
341
|
+
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
342
|
+
: indexedData;
|
|
343
|
+
// Create frame with just the image data (not full canvas)
|
|
344
|
+
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
345
|
+
for (let y = 0; y < imageHeight; y++) {
|
|
346
|
+
for (let x = 0; x < imageWidth; x++) {
|
|
347
|
+
const srcIdx = y * imageWidth + x;
|
|
348
|
+
if (srcIdx >= deinterlaced.length)
|
|
349
|
+
continue;
|
|
350
|
+
const colorIndex = deinterlaced[srcIdx];
|
|
351
|
+
const dstIdx = (y * imageWidth + x) * 4;
|
|
352
|
+
if (transparentColorIndex !== null &&
|
|
353
|
+
colorIndex === transparentColorIndex) {
|
|
354
|
+
// Transparent pixel
|
|
355
|
+
frameData[dstIdx] = 0;
|
|
356
|
+
frameData[dstIdx + 1] = 0;
|
|
357
|
+
frameData[dstIdx + 2] = 0;
|
|
358
|
+
frameData[dstIdx + 3] = 0;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Copy color from color table
|
|
362
|
+
const colorOffset = colorIndex * 3;
|
|
363
|
+
if (colorOffset + 2 < colorTable.length) {
|
|
364
|
+
frameData[dstIdx] = colorTable[colorOffset];
|
|
365
|
+
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
366
|
+
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
367
|
+
frameData[dstIdx + 3] = 255;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
frames.push({
|
|
373
|
+
width: imageWidth,
|
|
374
|
+
height: imageHeight,
|
|
375
|
+
left: imageLeft,
|
|
376
|
+
top: imageTop,
|
|
377
|
+
data: frameData,
|
|
378
|
+
delay: delayTime,
|
|
379
|
+
disposal: disposalMethod,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
356
382
|
deinterlace(data, width, height) {
|
|
357
383
|
const deinterlaced = new Uint8Array(data.length);
|
|
358
384
|
const passes = [
|
|
@@ -24,6 +24,19 @@ exports.rotate180 = rotate180;
|
|
|
24
24
|
exports.rotate270 = rotate270;
|
|
25
25
|
exports.flipHorizontal = flipHorizontal;
|
|
26
26
|
exports.flipVertical = flipVertical;
|
|
27
|
+
/**
|
|
28
|
+
* Detect system endianness
|
|
29
|
+
* Returns true if little-endian (most common), false if big-endian
|
|
30
|
+
*/
|
|
31
|
+
function isLittleEndian() {
|
|
32
|
+
const buffer = new ArrayBuffer(4);
|
|
33
|
+
const uint32View = new Uint32Array(buffer);
|
|
34
|
+
const uint8View = new Uint8Array(buffer);
|
|
35
|
+
uint32View[0] = 0x01020304;
|
|
36
|
+
return uint8View[0] === 0x04;
|
|
37
|
+
}
|
|
38
|
+
// Cache the endianness check result
|
|
39
|
+
const IS_LITTLE_ENDIAN = isLittleEndian();
|
|
27
40
|
/**
|
|
28
41
|
* Composite one image on top of another at a specified position
|
|
29
42
|
* @param base Base image data (RGBA)
|
|
@@ -48,17 +61,20 @@ function composite(base, baseWidth, baseHeight, overlay, overlayWidth, overlayHe
|
|
|
48
61
|
const endY = Math.min(baseHeight, y + overlayHeight);
|
|
49
62
|
// Iterate over the overlapping region
|
|
50
63
|
for (let py = startY; py < endY; py++) {
|
|
64
|
+
const baseRowOffset = py * baseWidth * 4;
|
|
65
|
+
const overlayRowOffset = (py - y) * overlayWidth * 4;
|
|
51
66
|
for (let px = startX; px < endX; px++) {
|
|
52
|
-
// Calculate indices
|
|
53
|
-
const baseIdx =
|
|
54
|
-
const
|
|
55
|
-
const overlayY = py - y;
|
|
56
|
-
const overlayIdx = (overlayY * overlayWidth + overlayX) * 4;
|
|
67
|
+
// Calculate indices with pre-computed offsets
|
|
68
|
+
const baseIdx = baseRowOffset + px * 4;
|
|
69
|
+
const overlayIdx = overlayRowOffset + (px - x) * 4;
|
|
57
70
|
// Get overlay pixel with opacity
|
|
58
71
|
const overlayR = overlay[overlayIdx];
|
|
59
72
|
const overlayG = overlay[overlayIdx + 1];
|
|
60
73
|
const overlayB = overlay[overlayIdx + 2];
|
|
61
74
|
const overlayA = (overlay[overlayIdx + 3] / 255) * finalOpacity;
|
|
75
|
+
// Skip if overlay is fully transparent
|
|
76
|
+
if (overlayA === 0)
|
|
77
|
+
continue;
|
|
62
78
|
// Get base pixel
|
|
63
79
|
const baseR = result[baseIdx];
|
|
64
80
|
const baseG = result[baseIdx + 1];
|
|
@@ -66,11 +82,14 @@ function composite(base, baseWidth, baseHeight, overlay, overlayWidth, overlayHe
|
|
|
66
82
|
const baseA = result[baseIdx + 3] / 255;
|
|
67
83
|
// Alpha compositing using "over" operation
|
|
68
84
|
const outA = overlayA + baseA * (1 - overlayA);
|
|
69
|
-
if (outA > 0) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
result[baseIdx
|
|
85
|
+
if (outA > 0.001) {
|
|
86
|
+
const invOverlayA = 1 - overlayA;
|
|
87
|
+
const baseWeight = baseA * invOverlayA;
|
|
88
|
+
const invOutA = 1 / outA;
|
|
89
|
+
result[baseIdx] = ((overlayR * overlayA + baseR * baseWeight) * invOutA + 0.5) | 0;
|
|
90
|
+
result[baseIdx + 1] = ((overlayG * overlayA + baseG * baseWeight) * invOutA + 0.5) | 0;
|
|
91
|
+
result[baseIdx + 2] = ((overlayB * overlayA + baseB * baseWeight) * invOutA + 0.5) | 0;
|
|
92
|
+
result[baseIdx + 3] = (outA * 255 + 0.5) | 0;
|
|
74
93
|
}
|
|
75
94
|
}
|
|
76
95
|
}
|
|
@@ -84,11 +103,23 @@ function composite(base, baseWidth, baseHeight, overlay, overlayWidth, overlayHe
|
|
|
84
103
|
*/
|
|
85
104
|
function adjustBrightness(data, amount) {
|
|
86
105
|
const result = new Uint8Array(data.length);
|
|
87
|
-
const
|
|
106
|
+
const clampedAmount = Math.max(-1, Math.min(1, amount));
|
|
107
|
+
const adjust = clampedAmount * 255;
|
|
108
|
+
// Pre-compute lookup table for clamping
|
|
109
|
+
// Range: -255 to 511 (data value 0-255 + adjust -255 to 255), offset by 255 for zero-based index
|
|
110
|
+
const LUT_SIZE = 767;
|
|
111
|
+
const LUT_OFFSET = 255;
|
|
112
|
+
const lut = new Uint8Array(LUT_SIZE);
|
|
113
|
+
for (let i = 0; i < LUT_SIZE; i++) {
|
|
114
|
+
const value = i - LUT_OFFSET;
|
|
115
|
+
lut[i] = value < 0 ? 0 : (value > 255 ? 255 : value);
|
|
116
|
+
}
|
|
117
|
+
// Use bitwise OR for fast rounding (equivalent to Math.round for positive numbers)
|
|
118
|
+
const adjustInt = (adjust + 0.5) | 0;
|
|
88
119
|
for (let i = 0; i < data.length; i += 4) {
|
|
89
|
-
result[i] =
|
|
90
|
-
result[i + 1] =
|
|
91
|
-
result[i + 2] =
|
|
120
|
+
result[i] = lut[data[i] + adjustInt + LUT_OFFSET]; // R
|
|
121
|
+
result[i + 1] = lut[data[i + 1] + adjustInt + LUT_OFFSET]; // G
|
|
122
|
+
result[i + 2] = lut[data[i + 2] + adjustInt + LUT_OFFSET]; // B
|
|
92
123
|
result[i + 3] = data[i + 3]; // A
|
|
93
124
|
}
|
|
94
125
|
return result;
|
|
@@ -104,10 +135,16 @@ function adjustContrast(data, amount) {
|
|
|
104
135
|
const contrast = Math.max(-1, Math.min(1, amount));
|
|
105
136
|
const factor = (259 * (contrast * 255 + 255)) /
|
|
106
137
|
(255 * (259 - contrast * 255));
|
|
138
|
+
// Pre-compute lookup table for all possible pixel values
|
|
139
|
+
const lut = new Uint8Array(256);
|
|
140
|
+
for (let i = 0; i < 256; i++) {
|
|
141
|
+
const val = factor * (i - 128) + 128;
|
|
142
|
+
lut[i] = val < 0 ? 0 : (val > 255 ? 255 : Math.round(val));
|
|
143
|
+
}
|
|
107
144
|
for (let i = 0; i < data.length; i += 4) {
|
|
108
|
-
result[i] =
|
|
109
|
-
result[i + 1] =
|
|
110
|
-
result[i + 2] =
|
|
145
|
+
result[i] = lut[data[i]]; // R
|
|
146
|
+
result[i + 1] = lut[data[i + 1]]; // G
|
|
147
|
+
result[i + 2] = lut[data[i + 2]]; // B
|
|
111
148
|
result[i + 3] = data[i + 3]; // A
|
|
112
149
|
}
|
|
113
150
|
return result;
|
|
@@ -122,10 +159,16 @@ function adjustExposure(data, amount) {
|
|
|
122
159
|
const result = new Uint8Array(data.length);
|
|
123
160
|
const stops = Math.max(-3, Math.min(3, amount));
|
|
124
161
|
const multiplier = Math.pow(2, stops);
|
|
162
|
+
// Pre-compute lookup table for all possible pixel values
|
|
163
|
+
const lut = new Uint8Array(256);
|
|
164
|
+
for (let i = 0; i < 256; i++) {
|
|
165
|
+
const val = i * multiplier;
|
|
166
|
+
lut[i] = val > 255 ? 255 : (val + 0.5) | 0;
|
|
167
|
+
}
|
|
125
168
|
for (let i = 0; i < data.length; i += 4) {
|
|
126
|
-
result[i] =
|
|
127
|
-
result[i + 1] =
|
|
128
|
-
result[i + 2] =
|
|
169
|
+
result[i] = lut[data[i]]; // R
|
|
170
|
+
result[i + 1] = lut[data[i + 1]]; // G
|
|
171
|
+
result[i + 2] = lut[data[i + 2]]; // B
|
|
129
172
|
result[i + 3] = data[i + 3]; // A
|
|
130
173
|
}
|
|
131
174
|
return result;
|
|
@@ -332,15 +375,12 @@ function crop(data, width, height, x, y, cropWidth, cropHeight) {
|
|
|
332
375
|
const actualWidth = endX - startX;
|
|
333
376
|
const actualHeight = endY - startY;
|
|
334
377
|
const result = new Uint8Array(actualWidth * actualHeight * 4);
|
|
378
|
+
const rowBytes = actualWidth * 4;
|
|
379
|
+
// Copy entire rows at once for better performance
|
|
335
380
|
for (let py = 0; py < actualHeight; py++) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
result[dstIdx] = data[srcIdx];
|
|
340
|
-
result[dstIdx + 1] = data[srcIdx + 1];
|
|
341
|
-
result[dstIdx + 2] = data[srcIdx + 2];
|
|
342
|
-
result[dstIdx + 3] = data[srcIdx + 3];
|
|
343
|
-
}
|
|
381
|
+
const srcOffset = ((startY + py) * width + startX) * 4;
|
|
382
|
+
const dstOffset = py * rowBytes;
|
|
383
|
+
result.set(data.subarray(srcOffset, srcOffset + rowBytes), dstOffset);
|
|
344
384
|
}
|
|
345
385
|
return { data: result, width: actualWidth, height: actualHeight };
|
|
346
386
|
}
|
|
@@ -419,46 +459,52 @@ function generateGaussianKernel(radius, sigma) {
|
|
|
419
459
|
function gaussianBlur(data, width, height, radius = 1, sigma) {
|
|
420
460
|
const clampedRadius = Math.max(1, Math.floor(radius));
|
|
421
461
|
const kernel = generateGaussianKernel(clampedRadius, sigma);
|
|
462
|
+
const widthMinus1 = width - 1;
|
|
463
|
+
const heightMinus1 = height - 1;
|
|
422
464
|
// Apply horizontal pass
|
|
423
465
|
const temp = new Uint8Array(data.length);
|
|
424
466
|
for (let y = 0; y < height; y++) {
|
|
467
|
+
const rowOffset = y * width * 4;
|
|
425
468
|
for (let x = 0; x < width; x++) {
|
|
426
469
|
let r = 0, g = 0, b = 0, a = 0;
|
|
427
470
|
for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
|
|
428
|
-
const px =
|
|
429
|
-
const
|
|
471
|
+
const px = x + kx;
|
|
472
|
+
const clampedPx = px < 0 ? 0 : (px > widthMinus1 ? widthMinus1 : px);
|
|
473
|
+
const idx = rowOffset + clampedPx * 4;
|
|
430
474
|
const weight = kernel[kx + clampedRadius];
|
|
431
475
|
r += data[idx] * weight;
|
|
432
476
|
g += data[idx + 1] * weight;
|
|
433
477
|
b += data[idx + 2] * weight;
|
|
434
478
|
a += data[idx + 3] * weight;
|
|
435
479
|
}
|
|
436
|
-
const outIdx =
|
|
437
|
-
temp[outIdx] =
|
|
438
|
-
temp[outIdx + 1] =
|
|
439
|
-
temp[outIdx + 2] =
|
|
440
|
-
temp[outIdx + 3] =
|
|
480
|
+
const outIdx = rowOffset + x * 4;
|
|
481
|
+
temp[outIdx] = (r + 0.5) | 0;
|
|
482
|
+
temp[outIdx + 1] = (g + 0.5) | 0;
|
|
483
|
+
temp[outIdx + 2] = (b + 0.5) | 0;
|
|
484
|
+
temp[outIdx + 3] = (a + 0.5) | 0;
|
|
441
485
|
}
|
|
442
486
|
}
|
|
443
487
|
// Apply vertical pass
|
|
444
488
|
const result = new Uint8Array(data.length);
|
|
445
489
|
for (let y = 0; y < height; y++) {
|
|
490
|
+
const rowOffset = y * width * 4;
|
|
446
491
|
for (let x = 0; x < width; x++) {
|
|
447
492
|
let r = 0, g = 0, b = 0, a = 0;
|
|
448
493
|
for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
|
|
449
|
-
const py =
|
|
450
|
-
const
|
|
494
|
+
const py = y + ky;
|
|
495
|
+
const clampedPy = py < 0 ? 0 : (py > heightMinus1 ? heightMinus1 : py);
|
|
496
|
+
const idx = clampedPy * width * 4 + x * 4;
|
|
451
497
|
const weight = kernel[ky + clampedRadius];
|
|
452
498
|
r += temp[idx] * weight;
|
|
453
499
|
g += temp[idx + 1] * weight;
|
|
454
500
|
b += temp[idx + 2] * weight;
|
|
455
501
|
a += temp[idx + 3] * weight;
|
|
456
502
|
}
|
|
457
|
-
const outIdx =
|
|
458
|
-
result[outIdx] =
|
|
459
|
-
result[outIdx + 1] =
|
|
460
|
-
result[outIdx + 2] =
|
|
461
|
-
result[outIdx + 3] =
|
|
503
|
+
const outIdx = rowOffset + x * 4;
|
|
504
|
+
result[outIdx] = (r + 0.5) | 0;
|
|
505
|
+
result[outIdx + 1] = (g + 0.5) | 0;
|
|
506
|
+
result[outIdx + 2] = (b + 0.5) | 0;
|
|
507
|
+
result[outIdx + 3] = (a + 0.5) | 0;
|
|
462
508
|
}
|
|
463
509
|
}
|
|
464
510
|
return result;
|
|
@@ -608,16 +654,30 @@ function rotate90(data, width, height) {
|
|
|
608
654
|
*/
|
|
609
655
|
function rotate180(data, width, height) {
|
|
610
656
|
const result = new Uint8Array(data.length);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
657
|
+
// Only use Uint32Array optimization on little-endian systems to avoid byte order issues
|
|
658
|
+
if (IS_LITTLE_ENDIAN) {
|
|
659
|
+
// Use Uint32Array view for faster 4-byte (pixel) copying
|
|
660
|
+
// Note: Uint8Array buffers are guaranteed to be aligned for any TypedArray view
|
|
661
|
+
const src32 = new Uint32Array(data.buffer, data.byteOffset, width * height);
|
|
662
|
+
const dst32 = new Uint32Array(result.buffer, result.byteOffset, width * height);
|
|
663
|
+
const totalPixels = width * height;
|
|
664
|
+
for (let i = 0; i < totalPixels; i++) {
|
|
665
|
+
dst32[totalPixels - 1 - i] = src32[i];
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
// Fallback for big-endian systems - byte-by-byte copying
|
|
670
|
+
for (let y = 0; y < height; y++) {
|
|
671
|
+
for (let x = 0; x < width; x++) {
|
|
672
|
+
const srcIdx = (y * width + x) * 4;
|
|
673
|
+
const dstX = width - 1 - x;
|
|
674
|
+
const dstY = height - 1 - y;
|
|
675
|
+
const dstIdx = (dstY * width + dstX) * 4;
|
|
676
|
+
result[dstIdx] = data[srcIdx];
|
|
677
|
+
result[dstIdx + 1] = data[srcIdx + 1];
|
|
678
|
+
result[dstIdx + 2] = data[srcIdx + 2];
|
|
679
|
+
result[dstIdx + 3] = data[srcIdx + 3];
|
|
680
|
+
}
|
|
621
681
|
}
|
|
622
682
|
}
|
|
623
683
|
return result;
|
|
@@ -656,15 +716,33 @@ function rotate270(data, width, height) {
|
|
|
656
716
|
*/
|
|
657
717
|
function flipHorizontal(data, width, height) {
|
|
658
718
|
const result = new Uint8Array(data.length);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
719
|
+
// Only use Uint32Array optimization on little-endian systems to avoid byte order issues
|
|
720
|
+
if (IS_LITTLE_ENDIAN) {
|
|
721
|
+
// Use Uint32Array view for faster 4-byte (pixel) copying
|
|
722
|
+
// Note: Uint8Array buffers are guaranteed to be aligned for any TypedArray view
|
|
723
|
+
const src32 = new Uint32Array(data.buffer, data.byteOffset, width * height);
|
|
724
|
+
const dst32 = new Uint32Array(result.buffer, result.byteOffset, width * height);
|
|
725
|
+
for (let y = 0; y < height; y++) {
|
|
726
|
+
const rowStart = y * width;
|
|
727
|
+
for (let x = 0; x < width; x++) {
|
|
728
|
+
const srcIdx = rowStart + x;
|
|
729
|
+
const dstIdx = rowStart + (width - 1 - x);
|
|
730
|
+
dst32[dstIdx] = src32[srcIdx];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// Fallback for big-endian systems - byte-by-byte copying
|
|
736
|
+
for (let y = 0; y < height; y++) {
|
|
737
|
+
for (let x = 0; x < width; x++) {
|
|
738
|
+
const srcIdx = (y * width + x) * 4;
|
|
739
|
+
const dstX = width - 1 - x;
|
|
740
|
+
const dstIdx = (y * width + dstX) * 4;
|
|
741
|
+
result[dstIdx] = data[srcIdx];
|
|
742
|
+
result[dstIdx + 1] = data[srcIdx + 1];
|
|
743
|
+
result[dstIdx + 2] = data[srcIdx + 2];
|
|
744
|
+
result[dstIdx + 3] = data[srcIdx + 3];
|
|
745
|
+
}
|
|
668
746
|
}
|
|
669
747
|
}
|
|
670
748
|
return result;
|
|
@@ -678,16 +756,12 @@ function flipHorizontal(data, width, height) {
|
|
|
678
756
|
*/
|
|
679
757
|
function flipVertical(data, width, height) {
|
|
680
758
|
const result = new Uint8Array(data.length);
|
|
759
|
+
const rowBytes = width * 4;
|
|
760
|
+
// Copy entire rows at once for better performance
|
|
681
761
|
for (let y = 0; y < height; y++) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const dstIdx = (dstY * width + x) * 4;
|
|
686
|
-
result[dstIdx] = data[srcIdx];
|
|
687
|
-
result[dstIdx + 1] = data[srcIdx + 1];
|
|
688
|
-
result[dstIdx + 2] = data[srcIdx + 2];
|
|
689
|
-
result[dstIdx + 3] = data[srcIdx + 3];
|
|
690
|
-
}
|
|
762
|
+
const srcOffset = y * rowBytes;
|
|
763
|
+
const dstOffset = (height - 1 - y) * rowBytes;
|
|
764
|
+
result.set(data.subarray(srcOffset, srcOffset + rowBytes), dstOffset);
|
|
691
765
|
}
|
|
692
766
|
return result;
|
|
693
767
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Supports baseline DCT JPEG images (the most common format)
|
|
2
|
+
* JPEG decoder implementation supporting both baseline and progressive DCT
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Baseline DCT (SOF0) - Sequential encoding
|
|
6
|
+
* - Progressive DCT (SOF2) - Multi-scan encoding with spectral selection and successive approximation
|
|
7
|
+
*
|
|
8
|
+
* This is a pure JavaScript implementation that handles common JPEG files.
|
|
6
9
|
* For complex or non-standard JPEGs, the ImageDecoder API fallback is preferred.
|
|
7
10
|
*/
|
|
11
|
+
import type { ImageDecoderOptions } from "../types.js";
|
|
8
12
|
export declare class JPEGDecoder {
|
|
9
13
|
private data;
|
|
10
14
|
private pos;
|
|
@@ -17,7 +21,15 @@ export declare class JPEGDecoder {
|
|
|
17
21
|
private restartInterval;
|
|
18
22
|
private bitBuffer;
|
|
19
23
|
private bitCount;
|
|
20
|
-
|
|
24
|
+
private options;
|
|
25
|
+
private isProgressive;
|
|
26
|
+
private spectralStart;
|
|
27
|
+
private spectralEnd;
|
|
28
|
+
private successiveHigh;
|
|
29
|
+
private successiveLow;
|
|
30
|
+
private scanComponentIds;
|
|
31
|
+
private eobRun;
|
|
32
|
+
constructor(data: Uint8Array, settings?: ImageDecoderOptions);
|
|
21
33
|
decode(): Uint8Array;
|
|
22
34
|
private readMarker;
|
|
23
35
|
private readUint16;
|
|
@@ -33,6 +45,7 @@ export declare class JPEGDecoder {
|
|
|
33
45
|
private decodeHuffman;
|
|
34
46
|
private readBit;
|
|
35
47
|
private receiveBits;
|
|
48
|
+
private receiveUnsignedBits;
|
|
36
49
|
private idct;
|
|
37
50
|
private convertToRGB;
|
|
38
51
|
}
|