cross-image 0.2.4 → 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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +507 -333
  3. package/esm/mod.d.ts +4 -4
  4. package/esm/mod.js +2 -2
  5. package/esm/src/formats/apng.d.ts +5 -5
  6. package/esm/src/formats/apng.js +7 -9
  7. package/esm/src/formats/ascii.d.ts +3 -3
  8. package/esm/src/formats/ascii.js +1 -1
  9. package/esm/src/formats/avif.d.ts +3 -3
  10. package/esm/src/formats/avif.js +7 -7
  11. package/esm/src/formats/bmp.d.ts +3 -3
  12. package/esm/src/formats/bmp.js +2 -2
  13. package/esm/src/formats/dng.d.ts +1 -1
  14. package/esm/src/formats/dng.js +1 -1
  15. package/esm/src/formats/gif.d.ts +4 -4
  16. package/esm/src/formats/gif.js +14 -10
  17. package/esm/src/formats/heic.d.ts +3 -3
  18. package/esm/src/formats/heic.js +7 -7
  19. package/esm/src/formats/ico.d.ts +3 -3
  20. package/esm/src/formats/ico.js +4 -4
  21. package/esm/src/formats/jpeg.d.ts +3 -3
  22. package/esm/src/formats/jpeg.js +23 -11
  23. package/esm/src/formats/pam.d.ts +3 -3
  24. package/esm/src/formats/pam.js +2 -2
  25. package/esm/src/formats/pcx.d.ts +3 -3
  26. package/esm/src/formats/pcx.js +2 -2
  27. package/esm/src/formats/png.d.ts +3 -3
  28. package/esm/src/formats/png.js +2 -2
  29. package/esm/src/formats/png_base.js +2 -5
  30. package/esm/src/formats/ppm.d.ts +3 -3
  31. package/esm/src/formats/ppm.js +2 -2
  32. package/esm/src/formats/tiff.d.ts +7 -18
  33. package/esm/src/formats/tiff.js +86 -21
  34. package/esm/src/formats/webp.d.ts +3 -3
  35. package/esm/src/formats/webp.js +11 -8
  36. package/esm/src/image.d.ts +11 -3
  37. package/esm/src/image.js +37 -21
  38. package/esm/src/types.d.ts +56 -4
  39. package/esm/src/utils/gif_decoder.d.ts +4 -1
  40. package/esm/src/utils/gif_decoder.js +91 -65
  41. package/esm/src/utils/image_processing.js +144 -70
  42. package/esm/src/utils/jpeg_decoder.d.ts +17 -4
  43. package/esm/src/utils/jpeg_decoder.js +448 -83
  44. package/esm/src/utils/jpeg_encoder.d.ts +15 -1
  45. package/esm/src/utils/jpeg_encoder.js +263 -24
  46. package/esm/src/utils/resize.js +51 -20
  47. package/esm/src/utils/tiff_deflate.d.ts +18 -0
  48. package/esm/src/utils/tiff_deflate.js +27 -0
  49. package/esm/src/utils/tiff_packbits.d.ts +24 -0
  50. package/esm/src/utils/tiff_packbits.js +90 -0
  51. package/esm/src/utils/webp_decoder.d.ts +3 -1
  52. package/esm/src/utils/webp_decoder.js +144 -63
  53. package/esm/src/utils/webp_encoder.js +5 -11
  54. package/package.json +1 -1
  55. package/script/mod.d.ts +4 -4
  56. package/script/mod.js +2 -2
  57. package/script/src/formats/apng.d.ts +5 -5
  58. package/script/src/formats/apng.js +7 -9
  59. package/script/src/formats/ascii.d.ts +3 -3
  60. package/script/src/formats/ascii.js +1 -1
  61. package/script/src/formats/avif.d.ts +3 -3
  62. package/script/src/formats/avif.js +7 -7
  63. package/script/src/formats/bmp.d.ts +3 -3
  64. package/script/src/formats/bmp.js +2 -2
  65. package/script/src/formats/dng.d.ts +1 -1
  66. package/script/src/formats/dng.js +1 -1
  67. package/script/src/formats/gif.d.ts +4 -4
  68. package/script/src/formats/gif.js +14 -10
  69. package/script/src/formats/heic.d.ts +3 -3
  70. package/script/src/formats/heic.js +7 -7
  71. package/script/src/formats/ico.d.ts +3 -3
  72. package/script/src/formats/ico.js +4 -4
  73. package/script/src/formats/jpeg.d.ts +3 -3
  74. package/script/src/formats/jpeg.js +23 -11
  75. package/script/src/formats/pam.d.ts +3 -3
  76. package/script/src/formats/pam.js +2 -2
  77. package/script/src/formats/pcx.d.ts +3 -3
  78. package/script/src/formats/pcx.js +2 -2
  79. package/script/src/formats/png.d.ts +3 -3
  80. package/script/src/formats/png.js +2 -2
  81. package/script/src/formats/png_base.js +2 -5
  82. package/script/src/formats/ppm.d.ts +3 -3
  83. package/script/src/formats/ppm.js +2 -2
  84. package/script/src/formats/tiff.d.ts +7 -18
  85. package/script/src/formats/tiff.js +86 -21
  86. package/script/src/formats/webp.d.ts +3 -3
  87. package/script/src/formats/webp.js +11 -8
  88. package/script/src/image.d.ts +11 -3
  89. package/script/src/image.js +36 -20
  90. package/script/src/types.d.ts +56 -4
  91. package/script/src/utils/gif_decoder.d.ts +4 -1
  92. package/script/src/utils/gif_decoder.js +91 -65
  93. package/script/src/utils/image_processing.js +144 -70
  94. package/script/src/utils/jpeg_decoder.d.ts +17 -4
  95. package/script/src/utils/jpeg_decoder.js +448 -83
  96. package/script/src/utils/jpeg_encoder.d.ts +15 -1
  97. package/script/src/utils/jpeg_encoder.js +263 -24
  98. package/script/src/utils/resize.js +51 -20
  99. package/script/src/utils/tiff_deflate.d.ts +18 -0
  100. package/script/src/utils/tiff_deflate.js +31 -0
  101. package/script/src/utils/tiff_packbits.d.ts +24 -0
  102. package/script/src/utils/tiff_packbits.js +94 -0
  103. package/script/src/utils/webp_decoder.d.ts +3 -1
  104. package/script/src/utils/webp_decoder.js +144 -63
  105. package/script/src/utils/webp_encoder.js +5 -11
@@ -138,7 +138,7 @@ export interface ResizeOptions {
138
138
  /**
139
139
  * Options for ASCII art encoding
140
140
  */
141
- export interface ASCIIOptions {
141
+ export interface ASCIIEncoderOptions {
142
142
  /** Target width in characters (default: 80) */
143
143
  width?: number;
144
144
  /** Character set to use (default: "simple") */
@@ -148,10 +148,21 @@ export interface ASCIIOptions {
148
148
  /** Whether to invert brightness (default: false) */
149
149
  invert?: boolean;
150
150
  }
151
+ /**
152
+ * Options for TIFF encoding.
153
+ */
154
+ export interface TIFFEncoderOptions {
155
+ /** Compression method: "none" (default), "lzw", "packbits", or "deflate" */
156
+ compression?: "none" | "lzw" | "packbits" | "deflate";
157
+ /** Encode as grayscale instead of RGB/RGBA (default: false) */
158
+ grayscale?: boolean;
159
+ /** Encode as RGB without alpha channel (default: false, ignored if grayscale is true) */
160
+ rgb?: boolean;
161
+ }
151
162
  /**
152
163
  * Options for WebP encoding
153
164
  */
154
- export interface WebPEncodeOptions {
165
+ export interface WebPEncoderOptions {
155
166
  /**
156
167
  * Encoding quality (1-100, default: 90)
157
168
  * - 100 = lossless (VP8L)
@@ -164,6 +175,47 @@ export interface WebPEncodeOptions {
164
175
  */
165
176
  lossless?: boolean;
166
177
  }
178
+ /**
179
+ * Options for JPEG encoding.
180
+ */
181
+ export interface JPEGEncoderOptions {
182
+ /**
183
+ * Encoding quality (1-100, default depends on encoder backend).
184
+ */
185
+ quality?: number;
186
+ /**
187
+ * Enable progressive JPEG output when using the pure-JS encoder.
188
+ * Runtime encoders do not currently expose a progressive toggle.
189
+ */
190
+ progressive?: boolean;
191
+ }
192
+ /**
193
+ * Common options for decode APIs.
194
+ *
195
+ * These options are runtime-agnostic and can be passed to `Image.decode()` and
196
+ * `Image.decodeFrames()`.
197
+ */
198
+ export interface ImageDecoderOptions {
199
+ /**
200
+ * Controls tolerant decoding in the pure-JS decoders.
201
+ *
202
+ * - true (default): try to recover from corruption and continue
203
+ * - false: strict mode (fail fast)
204
+ */
205
+ tolerantDecoding?: boolean;
206
+ /**
207
+ * Optional warning callback used by pure-JS decoders when non-fatal issues are
208
+ * encountered.
209
+ */
210
+ onWarning?: (message: string, details?: unknown) => void;
211
+ /**
212
+ * Runtime decoder strategy.
213
+ *
214
+ * - "prefer" (default): try runtime decoders first (ImageDecoder/Canvas), then fall back to pure JS
215
+ * - "never": skip runtime decoders and use pure JS when available
216
+ */
217
+ runtimeDecoding?: "prefer" | "never";
218
+ }
167
219
  /**
168
220
  * Image format handler interface
169
221
  */
@@ -177,7 +229,7 @@ export interface ImageFormat {
177
229
  * @param data Raw image data
178
230
  * @returns Decoded image data
179
231
  */
180
- decode(data: Uint8Array): Promise<ImageData>;
232
+ decode(data: Uint8Array, options?: ImageDecoderOptions): Promise<ImageData>;
181
233
  /**
182
234
  * Encode image data to bytes
183
235
  * @param imageData Image data to encode
@@ -196,7 +248,7 @@ export interface ImageFormat {
196
248
  * @param data Raw image data
197
249
  * @returns Decoded multi-frame image data
198
250
  */
199
- decodeFrames?(data: Uint8Array): Promise<MultiFrameImageData>;
251
+ decodeFrames?(data: Uint8Array, options?: ImageDecoderOptions): Promise<MultiFrameImageData>;
200
252
  /**
201
253
  * Encode multi-frame image data to bytes (optional)
202
254
  * @param imageData Multi-frame image data to encode
@@ -2,6 +2,7 @@
2
2
  * Pure JavaScript GIF decoder implementation
3
3
  * Supports GIF87a and GIF89a formats with LZW decompression
4
4
  */
5
+ import type { ImageDecoderOptions } from "../types.js";
5
6
  interface GIFImage {
6
7
  width: number;
7
8
  height: number;
@@ -19,7 +20,8 @@ interface GIFFrame {
19
20
  export declare class GIFDecoder {
20
21
  private data;
21
22
  private pos;
22
- constructor(data: Uint8Array);
23
+ private options;
24
+ constructor(data: Uint8Array, settings?: ImageDecoderOptions);
23
25
  private readByte;
24
26
  private readUint16LE;
25
27
  private readBytes;
@@ -36,6 +38,7 @@ export declare class GIFDecoder {
36
38
  frames: GIFFrame[];
37
39
  };
38
40
  private indexedToRGBA;
41
+ private decodeFrame;
39
42
  private deinterlace;
40
43
  }
41
44
  export {};
@@ -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
- const imageLeft = this.readUint16LE();
211
- const imageTop = this.readUint16LE();
212
- const imageWidth = this.readUint16LE();
213
- const imageHeight = this.readUint16LE();
214
- const packed = this.readByte();
215
- const hasLocalColorTable = (packed & 0x80) !== 0;
216
- const interlaced = (packed & 0x40) !== 0;
217
- // Color table size: 2^(n+1) where n is the 3 least significant bits
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
- frames.push({
268
- width: imageWidth,
269
- height: imageHeight,
270
- left: imageLeft,
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 = (py * baseWidth + px) * 4;
32
- const overlayX = px - x;
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
- result[baseIdx] = Math.round((overlayR * overlayA + baseR * baseA * (1 - overlayA)) / outA);
49
- result[baseIdx + 1] = Math.round((overlayG * overlayA + baseG * baseA * (1 - overlayA)) / outA);
50
- result[baseIdx + 2] = Math.round((overlayB * overlayA + baseB * baseA * (1 - overlayA)) / outA);
51
- result[baseIdx + 3] = Math.round(outA * 255);
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 adjust = Math.max(-1, Math.min(1, amount)) * 255;
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] = Math.max(0, Math.min(255, data[i] + adjust)); // R
68
- result[i + 1] = Math.max(0, Math.min(255, data[i + 1] + adjust)); // G
69
- result[i + 2] = Math.max(0, Math.min(255, data[i + 2] + adjust)); // B
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] = Math.max(0, Math.min(255, factor * (data[i] - 128) + 128)); // R
87
- result[i + 1] = Math.max(0, Math.min(255, factor * (data[i + 1] - 128) + 128)); // G
88
- result[i + 2] = Math.max(0, Math.min(255, factor * (data[i + 2] - 128) + 128)); // B
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] = Math.max(0, Math.min(255, data[i] * multiplier)); // R
105
- result[i + 1] = Math.max(0, Math.min(255, data[i + 1] * multiplier)); // G
106
- result[i + 2] = Math.max(0, Math.min(255, data[i + 2] * multiplier)); // B
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
- for (let px = 0; px < actualWidth; px++) {
315
- const srcIdx = ((startY + py) * width + (startX + px)) * 4;
316
- const dstIdx = (py * actualWidth + px) * 4;
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 = Math.max(0, Math.min(width - 1, x + kx));
407
- const idx = (y * width + px) * 4;
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 = (y * width + x) * 4;
415
- temp[outIdx] = Math.round(r);
416
- temp[outIdx + 1] = Math.round(g);
417
- temp[outIdx + 2] = Math.round(b);
418
- temp[outIdx + 3] = Math.round(a);
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 = Math.max(0, Math.min(height - 1, y + ky));
428
- const idx = (py * width + x) * 4;
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 = (y * width + x) * 4;
436
- result[outIdx] = Math.round(r);
437
- result[outIdx + 1] = Math.round(g);
438
- result[outIdx + 2] = Math.round(b);
439
- result[outIdx + 3] = Math.round(a);
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
- for (let y = 0; y < height; y++) {
590
- for (let x = 0; x < width; x++) {
591
- const srcIdx = (y * width + x) * 4;
592
- const dstX = width - 1 - x;
593
- const dstY = height - 1 - y;
594
- const dstIdx = (dstY * width + dstX) * 4;
595
- result[dstIdx] = data[srcIdx];
596
- result[dstIdx + 1] = data[srcIdx + 1];
597
- result[dstIdx + 2] = data[srcIdx + 2];
598
- result[dstIdx + 3] = data[srcIdx + 3];
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
- for (let y = 0; y < height; y++) {
638
- for (let x = 0; x < width; x++) {
639
- const srcIdx = (y * width + x) * 4;
640
- const dstX = width - 1 - x;
641
- const dstIdx = (y * width + dstX) * 4;
642
- result[dstIdx] = data[srcIdx];
643
- result[dstIdx + 1] = data[srcIdx + 1];
644
- result[dstIdx + 2] = data[srcIdx + 2];
645
- result[dstIdx + 3] = data[srcIdx + 3];
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
- for (let x = 0; x < width; x++) {
661
- const srcIdx = (y * width + x) * 4;
662
- const dstY = height - 1 - y;
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
  }