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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +292 -74
  3. package/esm/mod.d.ts +6 -4
  4. package/esm/mod.js +4 -2
  5. package/esm/src/formats/apng.d.ts +17 -5
  6. package/esm/src/formats/apng.js +104 -9
  7. package/esm/src/formats/ascii.d.ts +13 -3
  8. package/esm/src/formats/ascii.js +25 -1
  9. package/esm/src/formats/avif.d.ts +96 -0
  10. package/esm/src/formats/avif.js +607 -0
  11. package/esm/src/formats/bmp.d.ts +13 -3
  12. package/esm/src/formats/bmp.js +75 -2
  13. package/esm/src/formats/dng.d.ts +14 -2
  14. package/esm/src/formats/dng.js +27 -5
  15. package/esm/src/formats/gif.d.ts +18 -5
  16. package/esm/src/formats/gif.js +160 -14
  17. package/esm/src/formats/heic.d.ts +96 -0
  18. package/esm/src/formats/heic.js +608 -0
  19. package/esm/src/formats/ico.d.ts +13 -3
  20. package/esm/src/formats/ico.js +32 -4
  21. package/esm/src/formats/jpeg.d.ts +10 -3
  22. package/esm/src/formats/jpeg.js +99 -11
  23. package/esm/src/formats/pam.d.ts +13 -3
  24. package/esm/src/formats/pam.js +68 -2
  25. package/esm/src/formats/pcx.d.ts +13 -3
  26. package/esm/src/formats/pcx.js +47 -2
  27. package/esm/src/formats/png.d.ts +15 -3
  28. package/esm/src/formats/png.js +89 -2
  29. package/esm/src/formats/png_base.js +2 -5
  30. package/esm/src/formats/ppm.d.ts +13 -3
  31. package/esm/src/formats/ppm.js +36 -2
  32. package/esm/src/formats/tiff.d.ts +14 -18
  33. package/esm/src/formats/tiff.js +219 -20
  34. package/esm/src/formats/webp.d.ts +10 -3
  35. package/esm/src/formats/webp.js +103 -8
  36. package/esm/src/image.d.ts +20 -3
  37. package/esm/src/image.js +65 -21
  38. package/esm/src/types.d.ts +74 -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 +18 -1
  55. package/script/mod.d.ts +6 -4
  56. package/script/mod.js +7 -3
  57. package/script/src/formats/apng.d.ts +17 -5
  58. package/script/src/formats/apng.js +104 -9
  59. package/script/src/formats/ascii.d.ts +13 -3
  60. package/script/src/formats/ascii.js +25 -1
  61. package/script/src/formats/avif.d.ts +96 -0
  62. package/script/src/formats/avif.js +611 -0
  63. package/script/src/formats/bmp.d.ts +13 -3
  64. package/script/src/formats/bmp.js +75 -2
  65. package/script/src/formats/dng.d.ts +14 -2
  66. package/script/src/formats/dng.js +27 -5
  67. package/script/src/formats/gif.d.ts +18 -5
  68. package/script/src/formats/gif.js +160 -14
  69. package/script/src/formats/heic.d.ts +96 -0
  70. package/script/src/formats/heic.js +612 -0
  71. package/script/src/formats/ico.d.ts +13 -3
  72. package/script/src/formats/ico.js +32 -4
  73. package/script/src/formats/jpeg.d.ts +10 -3
  74. package/script/src/formats/jpeg.js +99 -11
  75. package/script/src/formats/pam.d.ts +13 -3
  76. package/script/src/formats/pam.js +68 -2
  77. package/script/src/formats/pcx.d.ts +13 -3
  78. package/script/src/formats/pcx.js +47 -2
  79. package/script/src/formats/png.d.ts +15 -3
  80. package/script/src/formats/png.js +89 -2
  81. package/script/src/formats/png_base.js +2 -5
  82. package/script/src/formats/ppm.d.ts +13 -3
  83. package/script/src/formats/ppm.js +36 -2
  84. package/script/src/formats/tiff.d.ts +14 -18
  85. package/script/src/formats/tiff.js +219 -20
  86. package/script/src/formats/webp.d.ts +10 -3
  87. package/script/src/formats/webp.js +103 -8
  88. package/script/src/image.d.ts +20 -3
  89. package/script/src/image.js +64 -20
  90. package/script/src/types.d.ts +74 -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
@@ -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
- const imageLeft = this.readUint16LE();
214
- const imageTop = this.readUint16LE();
215
- const imageWidth = this.readUint16LE();
216
- const imageHeight = this.readUint16LE();
217
- const packed = this.readByte();
218
- const hasLocalColorTable = (packed & 0x80) !== 0;
219
- const interlaced = (packed & 0x40) !== 0;
220
- // Color table size: 2^(n+1) where n is the 3 least significant bits
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
- frames.push({
271
- width: imageWidth,
272
- height: imageHeight,
273
- left: imageLeft,
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 = (py * baseWidth + px) * 4;
54
- const overlayX = px - x;
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
- result[baseIdx] = Math.round((overlayR * overlayA + baseR * baseA * (1 - overlayA)) / outA);
71
- result[baseIdx + 1] = Math.round((overlayG * overlayA + baseG * baseA * (1 - overlayA)) / outA);
72
- result[baseIdx + 2] = Math.round((overlayB * overlayA + baseB * baseA * (1 - overlayA)) / outA);
73
- result[baseIdx + 3] = Math.round(outA * 255);
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 adjust = Math.max(-1, Math.min(1, amount)) * 255;
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] = Math.max(0, Math.min(255, data[i] + adjust)); // R
90
- result[i + 1] = Math.max(0, Math.min(255, data[i + 1] + adjust)); // G
91
- result[i + 2] = Math.max(0, Math.min(255, data[i + 2] + adjust)); // B
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] = Math.max(0, Math.min(255, factor * (data[i] - 128) + 128)); // R
109
- result[i + 1] = Math.max(0, Math.min(255, factor * (data[i + 1] - 128) + 128)); // G
110
- result[i + 2] = Math.max(0, Math.min(255, factor * (data[i + 2] - 128) + 128)); // B
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] = Math.max(0, Math.min(255, data[i] * multiplier)); // R
127
- result[i + 1] = Math.max(0, Math.min(255, data[i + 1] * multiplier)); // G
128
- result[i + 2] = Math.max(0, Math.min(255, data[i + 2] * multiplier)); // B
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
- for (let px = 0; px < actualWidth; px++) {
337
- const srcIdx = ((startY + py) * width + (startX + px)) * 4;
338
- const dstIdx = (py * actualWidth + px) * 4;
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 = Math.max(0, Math.min(width - 1, x + kx));
429
- const idx = (y * width + px) * 4;
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 = (y * width + x) * 4;
437
- temp[outIdx] = Math.round(r);
438
- temp[outIdx + 1] = Math.round(g);
439
- temp[outIdx + 2] = Math.round(b);
440
- temp[outIdx + 3] = Math.round(a);
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 = Math.max(0, Math.min(height - 1, y + ky));
450
- const idx = (py * width + x) * 4;
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 = (y * width + x) * 4;
458
- result[outIdx] = Math.round(r);
459
- result[outIdx + 1] = Math.round(g);
460
- result[outIdx + 2] = Math.round(b);
461
- result[outIdx + 3] = Math.round(a);
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
- for (let y = 0; y < height; y++) {
612
- for (let x = 0; x < width; x++) {
613
- const srcIdx = (y * width + x) * 4;
614
- const dstX = width - 1 - x;
615
- const dstY = height - 1 - y;
616
- const dstIdx = (dstY * width + dstX) * 4;
617
- result[dstIdx] = data[srcIdx];
618
- result[dstIdx + 1] = data[srcIdx + 1];
619
- result[dstIdx + 2] = data[srcIdx + 2];
620
- result[dstIdx + 3] = data[srcIdx + 3];
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
- for (let y = 0; y < height; y++) {
660
- for (let x = 0; x < width; x++) {
661
- const srcIdx = (y * width + x) * 4;
662
- const dstX = width - 1 - x;
663
- const dstIdx = (y * width + dstX) * 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];
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
- for (let x = 0; x < width; x++) {
683
- const srcIdx = (y * width + x) * 4;
684
- const dstY = height - 1 - y;
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
- * Basic baseline JPEG decoder implementation
3
- * Supports baseline DCT JPEG images (the most common format)
2
+ * JPEG decoder implementation supporting both baseline and progressive DCT
4
3
  *
5
- * This is a simplified implementation that handles common JPEG files.
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
- constructor(data: Uint8Array);
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
  }