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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { LZWDecoder } from "./lzw.js";
|
|
6
6
|
export class GIFDecoder {
|
|
7
|
-
constructor(data) {
|
|
7
|
+
constructor(data, settings = {}) {
|
|
8
8
|
Object.defineProperty(this, "data", {
|
|
9
9
|
enumerable: true,
|
|
10
10
|
configurable: true,
|
|
@@ -17,8 +17,18 @@ export class GIFDecoder {
|
|
|
17
17
|
writable: true,
|
|
18
18
|
value: void 0
|
|
19
19
|
});
|
|
20
|
+
Object.defineProperty(this, "options", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: void 0
|
|
25
|
+
});
|
|
20
26
|
this.data = data;
|
|
21
27
|
this.pos = 0;
|
|
28
|
+
this.options = {
|
|
29
|
+
tolerantDecoding: settings.tolerantDecoding ?? true,
|
|
30
|
+
onWarning: settings.onWarning,
|
|
31
|
+
};
|
|
22
32
|
}
|
|
23
33
|
readByte() {
|
|
24
34
|
if (this.pos >= this.data.length) {
|
|
@@ -207,72 +217,20 @@ export class GIFDecoder {
|
|
|
207
217
|
}
|
|
208
218
|
else if (separator === 0x2c) {
|
|
209
219
|
// Image Descriptor
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const localColorTableSize = 2 << (packed & 0x07);
|
|
219
|
-
let localColorTable = null;
|
|
220
|
-
if (hasLocalColorTable) {
|
|
221
|
-
localColorTable = this.readColorTable(localColorTableSize);
|
|
222
|
-
}
|
|
223
|
-
// Read image data
|
|
224
|
-
const minCodeSize = this.readByte();
|
|
225
|
-
const compressedData = this.readDataSubBlocks();
|
|
226
|
-
// Decompress using LZW
|
|
227
|
-
const decoder = new LZWDecoder(minCodeSize, compressedData);
|
|
228
|
-
const indexedData = decoder.decompress();
|
|
229
|
-
// Convert indexed to RGBA
|
|
230
|
-
const colorTable = localColorTable || globalColorTable;
|
|
231
|
-
if (!colorTable) {
|
|
232
|
-
throw new Error("No color table available");
|
|
233
|
-
}
|
|
234
|
-
// Deinterlace if necessary
|
|
235
|
-
const deinterlaced = interlaced
|
|
236
|
-
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
237
|
-
: indexedData;
|
|
238
|
-
// Create frame with just the image data (not full canvas)
|
|
239
|
-
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
240
|
-
for (let y = 0; y < imageHeight; y++) {
|
|
241
|
-
for (let x = 0; x < imageWidth; x++) {
|
|
242
|
-
const srcIdx = y * imageWidth + x;
|
|
243
|
-
if (srcIdx >= deinterlaced.length)
|
|
244
|
-
continue;
|
|
245
|
-
const colorIndex = deinterlaced[srcIdx];
|
|
246
|
-
const dstIdx = (y * imageWidth + x) * 4;
|
|
247
|
-
if (transparentColorIndex !== null &&
|
|
248
|
-
colorIndex === transparentColorIndex) {
|
|
249
|
-
// Transparent pixel
|
|
250
|
-
frameData[dstIdx] = 0;
|
|
251
|
-
frameData[dstIdx + 1] = 0;
|
|
252
|
-
frameData[dstIdx + 2] = 0;
|
|
253
|
-
frameData[dstIdx + 3] = 0;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
// Copy color from color table
|
|
257
|
-
const colorOffset = colorIndex * 3;
|
|
258
|
-
if (colorOffset + 2 < colorTable.length) {
|
|
259
|
-
frameData[dstIdx] = colorTable[colorOffset];
|
|
260
|
-
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
261
|
-
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
262
|
-
frameData[dstIdx + 3] = 255;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
220
|
+
if (this.options.tolerantDecoding) {
|
|
221
|
+
try {
|
|
222
|
+
this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
// Tolerant decoding: skip corrupted frames and continue
|
|
226
|
+
// This allows partial decoding of multi-frame GIFs with some bad frames
|
|
227
|
+
this.options.onWarning?.("GIF: Skipping corrupted frame", e);
|
|
265
228
|
}
|
|
266
229
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
top: imageTop,
|
|
272
|
-
data: frameData,
|
|
273
|
-
delay: delayTime,
|
|
274
|
-
disposal: disposalMethod,
|
|
275
|
-
});
|
|
230
|
+
else {
|
|
231
|
+
// Non-tolerant mode: throw on first error
|
|
232
|
+
this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
|
|
233
|
+
}
|
|
276
234
|
// Reset graphic control extension state
|
|
277
235
|
transparentColorIndex = null;
|
|
278
236
|
delayTime = 0;
|
|
@@ -350,6 +308,74 @@ export class GIFDecoder {
|
|
|
350
308
|
data: rgba,
|
|
351
309
|
};
|
|
352
310
|
}
|
|
311
|
+
decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod) {
|
|
312
|
+
const imageLeft = this.readUint16LE();
|
|
313
|
+
const imageTop = this.readUint16LE();
|
|
314
|
+
const imageWidth = this.readUint16LE();
|
|
315
|
+
const imageHeight = this.readUint16LE();
|
|
316
|
+
const packed = this.readByte();
|
|
317
|
+
const hasLocalColorTable = (packed & 0x80) !== 0;
|
|
318
|
+
const interlaced = (packed & 0x40) !== 0;
|
|
319
|
+
// Color table size: 2^(n+1) where n is the 3 least significant bits
|
|
320
|
+
const localColorTableSize = 2 << (packed & 0x07);
|
|
321
|
+
let localColorTable = null;
|
|
322
|
+
if (hasLocalColorTable) {
|
|
323
|
+
localColorTable = this.readColorTable(localColorTableSize);
|
|
324
|
+
}
|
|
325
|
+
// Read image data
|
|
326
|
+
const minCodeSize = this.readByte();
|
|
327
|
+
const compressedData = this.readDataSubBlocks();
|
|
328
|
+
// Decompress using LZW
|
|
329
|
+
const decoder = new LZWDecoder(minCodeSize, compressedData);
|
|
330
|
+
const indexedData = decoder.decompress();
|
|
331
|
+
// Convert indexed to RGBA
|
|
332
|
+
const colorTable = localColorTable || globalColorTable;
|
|
333
|
+
if (!colorTable) {
|
|
334
|
+
throw new Error("No color table available");
|
|
335
|
+
}
|
|
336
|
+
// Deinterlace if necessary
|
|
337
|
+
const deinterlaced = interlaced
|
|
338
|
+
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
339
|
+
: indexedData;
|
|
340
|
+
// Create frame with just the image data (not full canvas)
|
|
341
|
+
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
342
|
+
for (let y = 0; y < imageHeight; y++) {
|
|
343
|
+
for (let x = 0; x < imageWidth; x++) {
|
|
344
|
+
const srcIdx = y * imageWidth + x;
|
|
345
|
+
if (srcIdx >= deinterlaced.length)
|
|
346
|
+
continue;
|
|
347
|
+
const colorIndex = deinterlaced[srcIdx];
|
|
348
|
+
const dstIdx = (y * imageWidth + x) * 4;
|
|
349
|
+
if (transparentColorIndex !== null &&
|
|
350
|
+
colorIndex === transparentColorIndex) {
|
|
351
|
+
// Transparent pixel
|
|
352
|
+
frameData[dstIdx] = 0;
|
|
353
|
+
frameData[dstIdx + 1] = 0;
|
|
354
|
+
frameData[dstIdx + 2] = 0;
|
|
355
|
+
frameData[dstIdx + 3] = 0;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// Copy color from color table
|
|
359
|
+
const colorOffset = colorIndex * 3;
|
|
360
|
+
if (colorOffset + 2 < colorTable.length) {
|
|
361
|
+
frameData[dstIdx] = colorTable[colorOffset];
|
|
362
|
+
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
363
|
+
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
364
|
+
frameData[dstIdx + 3] = 255;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
frames.push({
|
|
370
|
+
width: imageWidth,
|
|
371
|
+
height: imageHeight,
|
|
372
|
+
left: imageLeft,
|
|
373
|
+
top: imageTop,
|
|
374
|
+
data: frameData,
|
|
375
|
+
delay: delayTime,
|
|
376
|
+
disposal: disposalMethod,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
353
379
|
deinterlace(data, width, height) {
|
|
354
380
|
const deinterlaced = new Uint8Array(data.length);
|
|
355
381
|
const passes = [
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
* Image processing utilities for common operations like compositing,
|
|
3
3
|
* level adjustments, and color manipulations.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Detect system endianness
|
|
7
|
+
* Returns true if little-endian (most common), false if big-endian
|
|
8
|
+
*/
|
|
9
|
+
function isLittleEndian() {
|
|
10
|
+
const buffer = new ArrayBuffer(4);
|
|
11
|
+
const uint32View = new Uint32Array(buffer);
|
|
12
|
+
const uint8View = new Uint8Array(buffer);
|
|
13
|
+
uint32View[0] = 0x01020304;
|
|
14
|
+
return uint8View[0] === 0x04;
|
|
15
|
+
}
|
|
16
|
+
// Cache the endianness check result
|
|
17
|
+
const IS_LITTLE_ENDIAN = isLittleEndian();
|
|
5
18
|
/**
|
|
6
19
|
* Composite one image on top of another at a specified position
|
|
7
20
|
* @param base Base image data (RGBA)
|
|
@@ -26,17 +39,20 @@ export function composite(base, baseWidth, baseHeight, overlay, overlayWidth, ov
|
|
|
26
39
|
const endY = Math.min(baseHeight, y + overlayHeight);
|
|
27
40
|
// Iterate over the overlapping region
|
|
28
41
|
for (let py = startY; py < endY; py++) {
|
|
42
|
+
const baseRowOffset = py * baseWidth * 4;
|
|
43
|
+
const overlayRowOffset = (py - y) * overlayWidth * 4;
|
|
29
44
|
for (let px = startX; px < endX; px++) {
|
|
30
|
-
// Calculate indices
|
|
31
|
-
const baseIdx =
|
|
32
|
-
const
|
|
33
|
-
const overlayY = py - y;
|
|
34
|
-
const overlayIdx = (overlayY * overlayWidth + overlayX) * 4;
|
|
45
|
+
// Calculate indices with pre-computed offsets
|
|
46
|
+
const baseIdx = baseRowOffset + px * 4;
|
|
47
|
+
const overlayIdx = overlayRowOffset + (px - x) * 4;
|
|
35
48
|
// Get overlay pixel with opacity
|
|
36
49
|
const overlayR = overlay[overlayIdx];
|
|
37
50
|
const overlayG = overlay[overlayIdx + 1];
|
|
38
51
|
const overlayB = overlay[overlayIdx + 2];
|
|
39
52
|
const overlayA = (overlay[overlayIdx + 3] / 255) * finalOpacity;
|
|
53
|
+
// Skip if overlay is fully transparent
|
|
54
|
+
if (overlayA === 0)
|
|
55
|
+
continue;
|
|
40
56
|
// Get base pixel
|
|
41
57
|
const baseR = result[baseIdx];
|
|
42
58
|
const baseG = result[baseIdx + 1];
|
|
@@ -44,11 +60,14 @@ export function composite(base, baseWidth, baseHeight, overlay, overlayWidth, ov
|
|
|
44
60
|
const baseA = result[baseIdx + 3] / 255;
|
|
45
61
|
// Alpha compositing using "over" operation
|
|
46
62
|
const outA = overlayA + baseA * (1 - overlayA);
|
|
47
|
-
if (outA > 0) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
result[baseIdx
|
|
63
|
+
if (outA > 0.001) {
|
|
64
|
+
const invOverlayA = 1 - overlayA;
|
|
65
|
+
const baseWeight = baseA * invOverlayA;
|
|
66
|
+
const invOutA = 1 / outA;
|
|
67
|
+
result[baseIdx] = ((overlayR * overlayA + baseR * baseWeight) * invOutA + 0.5) | 0;
|
|
68
|
+
result[baseIdx + 1] = ((overlayG * overlayA + baseG * baseWeight) * invOutA + 0.5) | 0;
|
|
69
|
+
result[baseIdx + 2] = ((overlayB * overlayA + baseB * baseWeight) * invOutA + 0.5) | 0;
|
|
70
|
+
result[baseIdx + 3] = (outA * 255 + 0.5) | 0;
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
73
|
}
|
|
@@ -62,11 +81,23 @@ export function composite(base, baseWidth, baseHeight, overlay, overlayWidth, ov
|
|
|
62
81
|
*/
|
|
63
82
|
export function adjustBrightness(data, amount) {
|
|
64
83
|
const result = new Uint8Array(data.length);
|
|
65
|
-
const
|
|
84
|
+
const clampedAmount = Math.max(-1, Math.min(1, amount));
|
|
85
|
+
const adjust = clampedAmount * 255;
|
|
86
|
+
// Pre-compute lookup table for clamping
|
|
87
|
+
// Range: -255 to 511 (data value 0-255 + adjust -255 to 255), offset by 255 for zero-based index
|
|
88
|
+
const LUT_SIZE = 767;
|
|
89
|
+
const LUT_OFFSET = 255;
|
|
90
|
+
const lut = new Uint8Array(LUT_SIZE);
|
|
91
|
+
for (let i = 0; i < LUT_SIZE; i++) {
|
|
92
|
+
const value = i - LUT_OFFSET;
|
|
93
|
+
lut[i] = value < 0 ? 0 : (value > 255 ? 255 : value);
|
|
94
|
+
}
|
|
95
|
+
// Use bitwise OR for fast rounding (equivalent to Math.round for positive numbers)
|
|
96
|
+
const adjustInt = (adjust + 0.5) | 0;
|
|
66
97
|
for (let i = 0; i < data.length; i += 4) {
|
|
67
|
-
result[i] =
|
|
68
|
-
result[i + 1] =
|
|
69
|
-
result[i + 2] =
|
|
98
|
+
result[i] = lut[data[i] + adjustInt + LUT_OFFSET]; // R
|
|
99
|
+
result[i + 1] = lut[data[i + 1] + adjustInt + LUT_OFFSET]; // G
|
|
100
|
+
result[i + 2] = lut[data[i + 2] + adjustInt + LUT_OFFSET]; // B
|
|
70
101
|
result[i + 3] = data[i + 3]; // A
|
|
71
102
|
}
|
|
72
103
|
return result;
|
|
@@ -82,10 +113,16 @@ export function adjustContrast(data, amount) {
|
|
|
82
113
|
const contrast = Math.max(-1, Math.min(1, amount));
|
|
83
114
|
const factor = (259 * (contrast * 255 + 255)) /
|
|
84
115
|
(255 * (259 - contrast * 255));
|
|
116
|
+
// Pre-compute lookup table for all possible pixel values
|
|
117
|
+
const lut = new Uint8Array(256);
|
|
118
|
+
for (let i = 0; i < 256; i++) {
|
|
119
|
+
const val = factor * (i - 128) + 128;
|
|
120
|
+
lut[i] = val < 0 ? 0 : (val > 255 ? 255 : Math.round(val));
|
|
121
|
+
}
|
|
85
122
|
for (let i = 0; i < data.length; i += 4) {
|
|
86
|
-
result[i] =
|
|
87
|
-
result[i + 1] =
|
|
88
|
-
result[i + 2] =
|
|
123
|
+
result[i] = lut[data[i]]; // R
|
|
124
|
+
result[i + 1] = lut[data[i + 1]]; // G
|
|
125
|
+
result[i + 2] = lut[data[i + 2]]; // B
|
|
89
126
|
result[i + 3] = data[i + 3]; // A
|
|
90
127
|
}
|
|
91
128
|
return result;
|
|
@@ -100,10 +137,16 @@ export function adjustExposure(data, amount) {
|
|
|
100
137
|
const result = new Uint8Array(data.length);
|
|
101
138
|
const stops = Math.max(-3, Math.min(3, amount));
|
|
102
139
|
const multiplier = Math.pow(2, stops);
|
|
140
|
+
// Pre-compute lookup table for all possible pixel values
|
|
141
|
+
const lut = new Uint8Array(256);
|
|
142
|
+
for (let i = 0; i < 256; i++) {
|
|
143
|
+
const val = i * multiplier;
|
|
144
|
+
lut[i] = val > 255 ? 255 : (val + 0.5) | 0;
|
|
145
|
+
}
|
|
103
146
|
for (let i = 0; i < data.length; i += 4) {
|
|
104
|
-
result[i] =
|
|
105
|
-
result[i + 1] =
|
|
106
|
-
result[i + 2] =
|
|
147
|
+
result[i] = lut[data[i]]; // R
|
|
148
|
+
result[i + 1] = lut[data[i + 1]]; // G
|
|
149
|
+
result[i + 2] = lut[data[i + 2]]; // B
|
|
107
150
|
result[i + 3] = data[i + 3]; // A
|
|
108
151
|
}
|
|
109
152
|
return result;
|
|
@@ -310,15 +353,12 @@ export function crop(data, width, height, x, y, cropWidth, cropHeight) {
|
|
|
310
353
|
const actualWidth = endX - startX;
|
|
311
354
|
const actualHeight = endY - startY;
|
|
312
355
|
const result = new Uint8Array(actualWidth * actualHeight * 4);
|
|
356
|
+
const rowBytes = actualWidth * 4;
|
|
357
|
+
// Copy entire rows at once for better performance
|
|
313
358
|
for (let py = 0; py < actualHeight; py++) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
result[dstIdx] = data[srcIdx];
|
|
318
|
-
result[dstIdx + 1] = data[srcIdx + 1];
|
|
319
|
-
result[dstIdx + 2] = data[srcIdx + 2];
|
|
320
|
-
result[dstIdx + 3] = data[srcIdx + 3];
|
|
321
|
-
}
|
|
359
|
+
const srcOffset = ((startY + py) * width + startX) * 4;
|
|
360
|
+
const dstOffset = py * rowBytes;
|
|
361
|
+
result.set(data.subarray(srcOffset, srcOffset + rowBytes), dstOffset);
|
|
322
362
|
}
|
|
323
363
|
return { data: result, width: actualWidth, height: actualHeight };
|
|
324
364
|
}
|
|
@@ -397,46 +437,52 @@ function generateGaussianKernel(radius, sigma) {
|
|
|
397
437
|
export function gaussianBlur(data, width, height, radius = 1, sigma) {
|
|
398
438
|
const clampedRadius = Math.max(1, Math.floor(radius));
|
|
399
439
|
const kernel = generateGaussianKernel(clampedRadius, sigma);
|
|
440
|
+
const widthMinus1 = width - 1;
|
|
441
|
+
const heightMinus1 = height - 1;
|
|
400
442
|
// Apply horizontal pass
|
|
401
443
|
const temp = new Uint8Array(data.length);
|
|
402
444
|
for (let y = 0; y < height; y++) {
|
|
445
|
+
const rowOffset = y * width * 4;
|
|
403
446
|
for (let x = 0; x < width; x++) {
|
|
404
447
|
let r = 0, g = 0, b = 0, a = 0;
|
|
405
448
|
for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
|
|
406
|
-
const px =
|
|
407
|
-
const
|
|
449
|
+
const px = x + kx;
|
|
450
|
+
const clampedPx = px < 0 ? 0 : (px > widthMinus1 ? widthMinus1 : px);
|
|
451
|
+
const idx = rowOffset + clampedPx * 4;
|
|
408
452
|
const weight = kernel[kx + clampedRadius];
|
|
409
453
|
r += data[idx] * weight;
|
|
410
454
|
g += data[idx + 1] * weight;
|
|
411
455
|
b += data[idx + 2] * weight;
|
|
412
456
|
a += data[idx + 3] * weight;
|
|
413
457
|
}
|
|
414
|
-
const outIdx =
|
|
415
|
-
temp[outIdx] =
|
|
416
|
-
temp[outIdx + 1] =
|
|
417
|
-
temp[outIdx + 2] =
|
|
418
|
-
temp[outIdx + 3] =
|
|
458
|
+
const outIdx = rowOffset + x * 4;
|
|
459
|
+
temp[outIdx] = (r + 0.5) | 0;
|
|
460
|
+
temp[outIdx + 1] = (g + 0.5) | 0;
|
|
461
|
+
temp[outIdx + 2] = (b + 0.5) | 0;
|
|
462
|
+
temp[outIdx + 3] = (a + 0.5) | 0;
|
|
419
463
|
}
|
|
420
464
|
}
|
|
421
465
|
// Apply vertical pass
|
|
422
466
|
const result = new Uint8Array(data.length);
|
|
423
467
|
for (let y = 0; y < height; y++) {
|
|
468
|
+
const rowOffset = y * width * 4;
|
|
424
469
|
for (let x = 0; x < width; x++) {
|
|
425
470
|
let r = 0, g = 0, b = 0, a = 0;
|
|
426
471
|
for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
|
|
427
|
-
const py =
|
|
428
|
-
const
|
|
472
|
+
const py = y + ky;
|
|
473
|
+
const clampedPy = py < 0 ? 0 : (py > heightMinus1 ? heightMinus1 : py);
|
|
474
|
+
const idx = clampedPy * width * 4 + x * 4;
|
|
429
475
|
const weight = kernel[ky + clampedRadius];
|
|
430
476
|
r += temp[idx] * weight;
|
|
431
477
|
g += temp[idx + 1] * weight;
|
|
432
478
|
b += temp[idx + 2] * weight;
|
|
433
479
|
a += temp[idx + 3] * weight;
|
|
434
480
|
}
|
|
435
|
-
const outIdx =
|
|
436
|
-
result[outIdx] =
|
|
437
|
-
result[outIdx + 1] =
|
|
438
|
-
result[outIdx + 2] =
|
|
439
|
-
result[outIdx + 3] =
|
|
481
|
+
const outIdx = rowOffset + x * 4;
|
|
482
|
+
result[outIdx] = (r + 0.5) | 0;
|
|
483
|
+
result[outIdx + 1] = (g + 0.5) | 0;
|
|
484
|
+
result[outIdx + 2] = (b + 0.5) | 0;
|
|
485
|
+
result[outIdx + 3] = (a + 0.5) | 0;
|
|
440
486
|
}
|
|
441
487
|
}
|
|
442
488
|
return result;
|
|
@@ -586,16 +632,30 @@ export function rotate90(data, width, height) {
|
|
|
586
632
|
*/
|
|
587
633
|
export function rotate180(data, width, height) {
|
|
588
634
|
const result = new Uint8Array(data.length);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
635
|
+
// Only use Uint32Array optimization on little-endian systems to avoid byte order issues
|
|
636
|
+
if (IS_LITTLE_ENDIAN) {
|
|
637
|
+
// Use Uint32Array view for faster 4-byte (pixel) copying
|
|
638
|
+
// Note: Uint8Array buffers are guaranteed to be aligned for any TypedArray view
|
|
639
|
+
const src32 = new Uint32Array(data.buffer, data.byteOffset, width * height);
|
|
640
|
+
const dst32 = new Uint32Array(result.buffer, result.byteOffset, width * height);
|
|
641
|
+
const totalPixels = width * height;
|
|
642
|
+
for (let i = 0; i < totalPixels; i++) {
|
|
643
|
+
dst32[totalPixels - 1 - i] = src32[i];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// Fallback for big-endian systems - byte-by-byte copying
|
|
648
|
+
for (let y = 0; y < height; y++) {
|
|
649
|
+
for (let x = 0; x < width; x++) {
|
|
650
|
+
const srcIdx = (y * width + x) * 4;
|
|
651
|
+
const dstX = width - 1 - x;
|
|
652
|
+
const dstY = height - 1 - y;
|
|
653
|
+
const dstIdx = (dstY * width + dstX) * 4;
|
|
654
|
+
result[dstIdx] = data[srcIdx];
|
|
655
|
+
result[dstIdx + 1] = data[srcIdx + 1];
|
|
656
|
+
result[dstIdx + 2] = data[srcIdx + 2];
|
|
657
|
+
result[dstIdx + 3] = data[srcIdx + 3];
|
|
658
|
+
}
|
|
599
659
|
}
|
|
600
660
|
}
|
|
601
661
|
return result;
|
|
@@ -634,15 +694,33 @@ export function rotate270(data, width, height) {
|
|
|
634
694
|
*/
|
|
635
695
|
export function flipHorizontal(data, width, height) {
|
|
636
696
|
const result = new Uint8Array(data.length);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
697
|
+
// Only use Uint32Array optimization on little-endian systems to avoid byte order issues
|
|
698
|
+
if (IS_LITTLE_ENDIAN) {
|
|
699
|
+
// Use Uint32Array view for faster 4-byte (pixel) copying
|
|
700
|
+
// Note: Uint8Array buffers are guaranteed to be aligned for any TypedArray view
|
|
701
|
+
const src32 = new Uint32Array(data.buffer, data.byteOffset, width * height);
|
|
702
|
+
const dst32 = new Uint32Array(result.buffer, result.byteOffset, width * height);
|
|
703
|
+
for (let y = 0; y < height; y++) {
|
|
704
|
+
const rowStart = y * width;
|
|
705
|
+
for (let x = 0; x < width; x++) {
|
|
706
|
+
const srcIdx = rowStart + x;
|
|
707
|
+
const dstIdx = rowStart + (width - 1 - x);
|
|
708
|
+
dst32[dstIdx] = src32[srcIdx];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
// Fallback for big-endian systems - byte-by-byte copying
|
|
714
|
+
for (let y = 0; y < height; y++) {
|
|
715
|
+
for (let x = 0; x < width; x++) {
|
|
716
|
+
const srcIdx = (y * width + x) * 4;
|
|
717
|
+
const dstX = width - 1 - x;
|
|
718
|
+
const dstIdx = (y * width + dstX) * 4;
|
|
719
|
+
result[dstIdx] = data[srcIdx];
|
|
720
|
+
result[dstIdx + 1] = data[srcIdx + 1];
|
|
721
|
+
result[dstIdx + 2] = data[srcIdx + 2];
|
|
722
|
+
result[dstIdx + 3] = data[srcIdx + 3];
|
|
723
|
+
}
|
|
646
724
|
}
|
|
647
725
|
}
|
|
648
726
|
return result;
|
|
@@ -656,16 +734,12 @@ export function flipHorizontal(data, width, height) {
|
|
|
656
734
|
*/
|
|
657
735
|
export function flipVertical(data, width, height) {
|
|
658
736
|
const result = new Uint8Array(data.length);
|
|
737
|
+
const rowBytes = width * 4;
|
|
738
|
+
// Copy entire rows at once for better performance
|
|
659
739
|
for (let y = 0; y < height; y++) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const dstIdx = (dstY * width + x) * 4;
|
|
664
|
-
result[dstIdx] = data[srcIdx];
|
|
665
|
-
result[dstIdx + 1] = data[srcIdx + 1];
|
|
666
|
-
result[dstIdx + 2] = data[srcIdx + 2];
|
|
667
|
-
result[dstIdx + 3] = data[srcIdx + 3];
|
|
668
|
-
}
|
|
740
|
+
const srcOffset = y * rowBytes;
|
|
741
|
+
const dstOffset = (height - 1 - y) * rowBytes;
|
|
742
|
+
result.set(data.subarray(srcOffset, srcOffset + rowBytes), dstOffset);
|
|
669
743
|
}
|
|
670
744
|
return result;
|
|
671
745
|
}
|
|
@@ -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
|
}
|