ag-psd 33.0.0 → 34.0.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 (86) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  18. package/CHANGELOG.md +22 -0
  19. package/README.md +1 -1
  20. package/README_PSD.md +25 -9
  21. package/TODO.md +13 -84
  22. package/dist/abr.js +12 -7
  23. package/dist/abr.js.map +1 -1
  24. package/dist/additionalInfo.d.ts +2 -0
  25. package/dist/additionalInfo.js +361 -75
  26. package/dist/additionalInfo.js.map +1 -1
  27. package/dist/bundle.js +1524 -389
  28. package/dist/descriptor.d.ts +14 -5
  29. package/dist/descriptor.js +102 -64
  30. package/dist/descriptor.js.map +1 -1
  31. package/dist/helpers.d.ts +6 -0
  32. package/dist/helpers.js +242 -2
  33. package/dist/helpers.js.map +1 -1
  34. package/dist/imageResources.js +84 -3
  35. package/dist/imageResources.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/psd.d.ts +144 -2
  40. package/dist/psd.js.map +1 -1
  41. package/dist/psdReader.js +206 -110
  42. package/dist/psdReader.js.map +1 -1
  43. package/dist/psdWriter.d.ts +1 -0
  44. package/dist/psdWriter.js +336 -91
  45. package/dist/psdWriter.js.map +1 -1
  46. package/dist/text.d.ts +32 -1
  47. package/dist/text.js +58 -0
  48. package/dist/text.js.map +1 -1
  49. package/dist-es/abr.js +12 -7
  50. package/dist-es/abr.js.map +1 -1
  51. package/dist-es/additionalInfo.d.ts +2 -0
  52. package/dist-es/additionalInfo.js +361 -75
  53. package/dist-es/additionalInfo.js.map +1 -1
  54. package/dist-es/descriptor.d.ts +14 -5
  55. package/dist-es/descriptor.js +102 -64
  56. package/dist-es/descriptor.js.map +1 -1
  57. package/dist-es/helpers.d.ts +6 -0
  58. package/dist-es/helpers.js +242 -2
  59. package/dist-es/helpers.js.map +1 -1
  60. package/dist-es/imageResources.js +84 -3
  61. package/dist-es/imageResources.js.map +1 -1
  62. package/dist-es/index.d.ts +1 -0
  63. package/dist-es/index.js +1 -0
  64. package/dist-es/index.js.map +1 -1
  65. package/dist-es/psd.d.ts +144 -2
  66. package/dist-es/psd.js.map +1 -1
  67. package/dist-es/psdReader.js +206 -110
  68. package/dist-es/psdReader.js.map +1 -1
  69. package/dist-es/psdWriter.d.ts +1 -0
  70. package/dist-es/psdWriter.js +336 -91
  71. package/dist-es/psdWriter.js.map +1 -1
  72. package/dist-es/text.d.ts +32 -1
  73. package/dist-es/text.js +58 -0
  74. package/dist-es/text.js.map +1 -1
  75. package/package.json +1 -1
  76. package/perf.js +312 -9
  77. package/src/abr.ts +14 -7
  78. package/src/additionalInfo.ts +433 -87
  79. package/src/descriptor.ts +103 -58
  80. package/src/helpers.ts +262 -2
  81. package/src/imageResources.ts +107 -4
  82. package/src/index.ts +1 -0
  83. package/src/psd.ts +115 -2
  84. package/src/psdReader.ts +226 -108
  85. package/src/psdWriter.ts +345 -90
  86. package/src/text.ts +76 -1
package/src/psdReader.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Inflate } from 'pako';
2
2
  import { Psd, Layer, ColorMode, SectionDividerType, LayerAdditionalInfo, ReadOptions, LayerMaskData, Color, PatternInfo, GlobalLayerMaskInfo, RGB, PixelData, PixelArray, ChannelID, Compression } from './psd';
3
- import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, LayerMaskFlags, MaskParams, ColorSpace, largeAdditionalInfoKeys, largeAdditionalInfoKeysObserved, imageDataToCanvas, normalizeLabAB } from './helpers';
3
+ import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, LayerMaskFlags, MaskParams, ColorSpace, largeAdditionalInfoKeys, largeAdditionalInfoKeysObserved, imageDataToCanvas, normalizeLabAB, cmykToRgbValues } from './helpers';
4
4
  import { infoHandlersMap } from './additionalInfo';
5
5
  import { decodeString } from './utf8';
6
6
  import { InternalImageResources, resourceHandlersMap } from './imageResources';
@@ -185,6 +185,8 @@ export function readUnicodeString(reader: PsdReader) {
185
185
  }
186
186
 
187
187
  const STRING_CHUNK_SIZE = 0x2000;
188
+ // shorter strings (most descriptor keys, OSTypes and names) are faster to build with simple loop than String.fromCharCode.apply
189
+ const SHORT_STRING_LENGTH = 64;
188
190
 
189
191
  function checkReadBounds(reader: PsdReader, size: number) {
190
192
  if (reader.offset < 0 || (reader.offset + size) > reader.view.byteLength) {
@@ -198,10 +200,21 @@ function readUtf16String(reader: PsdReader, length: number, littleEndian: boolea
198
200
  checkReadBounds(reader, length * 2);
199
201
 
200
202
  const view = reader.view;
201
- const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
202
203
  let offset = reader.offset;
203
204
  let text = '';
204
205
 
206
+ if (length <= SHORT_STRING_LENGTH) {
207
+ for (let i = 0; i < length; i++, offset += 2) {
208
+ text += String.fromCharCode(view.getUint16(offset, littleEndian));
209
+ }
210
+
211
+ reader.offset = offset;
212
+ if (text.charCodeAt(text.length - 1) === 0) text = text.substring(0, text.length - 1);
213
+ return text;
214
+ }
215
+
216
+ const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
217
+
205
218
  for (let i = 0; i < length; i += STRING_CHUNK_SIZE) {
206
219
  const count = Math.min(STRING_CHUNK_SIZE, length - i);
207
220
 
@@ -241,6 +254,19 @@ export function readUnicodeStringWithLengthLE(reader: PsdReader, length: number)
241
254
  export function readAsciiString(reader: PsdReader, length: number) {
242
255
  if (!(length > 0)) return '';
243
256
  checkReadBounds(reader, length);
257
+
258
+ if (length <= SHORT_STRING_LENGTH) {
259
+ const view = reader.view;
260
+ let text = '';
261
+
262
+ for (let i = 0, offset = reader.offset; i < length; i++, offset++) {
263
+ text += String.fromCharCode(view.getUint8(offset));
264
+ }
265
+
266
+ reader.offset += length;
267
+ return text;
268
+ }
269
+
244
270
  const text = bytesToString(new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length));
245
271
  reader.offset += length;
246
272
  return text;
@@ -260,7 +286,16 @@ export function checkSignature(reader: PsdReader, a: string, b?: string) {
260
286
  }
261
287
 
262
288
  function readShortString(reader: PsdReader, length: number) {
263
- return bytesToString(readBytes(reader, length));
289
+ const bytes = readBytes(reader, length);
290
+ if (length > SHORT_STRING_LENGTH) return bytesToString(bytes);
291
+
292
+ let text = '';
293
+
294
+ for (let i = 0; i < bytes.length; i++) {
295
+ text += String.fromCharCode(bytes[i]);
296
+ }
297
+
298
+ return text;
264
299
  }
265
300
 
266
301
  function isValidSignature(sig: string) {
@@ -458,6 +493,10 @@ export function readLayerInfo(reader: PsdReader, psd: Psd, imageResources: Inter
458
493
  l.opened = type === SectionDividerType.OpenFolder;
459
494
  l.children = [];
460
495
 
496
+ // group layers can't have image data, some broken files have it, ignore it like Photoshop does
497
+ delete l.canvas;
498
+ delete l.imageData;
499
+
461
500
  if (l.sectionDivider?.key) {
462
501
  l.blendMode = toBlendMode[l.sectionDivider.key] ?? l.blendMode;
463
502
  }
@@ -675,8 +714,8 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
675
714
  let compression = Compression.RawData;
676
715
  let data: Uint8Array | undefined = undefined;
677
716
 
678
- if (channel.length === 1) throw new Error('Invalid channel length');
679
- if (channel.length) {
717
+ // channel length of 1 is invalid (should be 0 or at least 2), skip the byte and treat channel as empty
718
+ if (channel.length >= 2) {
680
719
  compression = readUint16(reader) as Compression;
681
720
  // try to fix broken files where there's 1 byte shift of channel
682
721
  if (compression > 3) {
@@ -702,7 +741,14 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
702
741
  }
703
742
 
704
743
  if (!reader.useRawData) {
705
- decodeLayerImageData(layer, reader);
744
+ try {
745
+ decodeLayerImageData(layer, reader);
746
+ } catch (e) {
747
+ // corrupted image data of one layer shouldn't prevent reading rest of the file
748
+ if (reader.throwForMissingFeatures || (e as Error).message === 'Exceeded memory limit') throw e;
749
+ reader.logMissingFeatures && reader.log(`Failed to decode layer image data: ${(e as Error).message}`);
750
+ delete layer.rawData;
751
+ }
706
752
  }
707
753
  }
708
754
 
@@ -924,7 +970,7 @@ export function readGlobalLayerMaskInfo(reader: PsdReader) {
924
970
  const colorSpace2 = readUint16(reader);
925
971
  const colorSpace3 = readUint16(reader);
926
972
  const colorSpace4 = readUint16(reader);
927
- const opacity = readUint16(reader) / 100; // 0 - 100
973
+ const opacity = Math.min(readUint16(reader), 100) / 100; // 0 - 100 (some files have values out of range)
928
974
  const kind = readUint8(reader);
929
975
  skipBytes(reader, left()); // 3 bytes of padding ?
930
976
  return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind };
@@ -1191,10 +1237,7 @@ function toRange(value: number, max: number) {
1191
1237
  return value <= 0 ? 0 : (value >= max ? max : value);
1192
1238
  }
1193
1239
 
1194
- // Polynomial approximation of U.S. Web Coated (SWOP) v2 CMYK to RGB conversion (default CMYK profile of Photoshop).
1195
- // Coefficients are taken from pdf.js (DeviceCmykCS in src/core/colorspace.js, https://github.com/mozilla/pdf.js),
1196
- // Copyright 2012 Mozilla Foundation, licensed under the Apache License, Version 2.0
1197
- // (http://www.apache.org/licenses/LICENSE-2.0)
1240
+ // converts CMYK data to RGB using approximation of U.S. Web Coated (SWOP) v2 profile (see cmykToRgbValues)
1198
1241
  // cmyk - 8 or 16 bit data with 5 channels (cmyka), rgb - 8 or 16 bit data with 4 channels (rgba)
1199
1242
  // removeMatte - colors are blended with white (no ink), used for composite image of documents with global alpha
1200
1243
  function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
@@ -1203,6 +1246,7 @@ function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
1203
1246
  const dstData = rgb.data;
1204
1247
  const srcMax = srcData instanceof Uint16Array ? 0xffff : 0xff;
1205
1248
  const dstMax = dstData instanceof Uint16Array ? 0xffff : 0xff;
1249
+ const out = [0, 0, 0];
1206
1250
 
1207
1251
  for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
1208
1252
  const a = srcData[src + 4];
@@ -1221,21 +1265,10 @@ function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
1221
1265
  k = Math.min(1, k * ra);
1222
1266
  }
1223
1267
 
1224
- dstData[dst] = toRange(255 +
1225
- c * (-4.387332384609988 * c + 54.48615194189176 * m + 18.82290502165302 * y + 212.25662451639585 * k - 285.2331026137004) +
1226
- m * (1.7149763477362134 * m - 5.6096736904047315 * y - 17.873870861415444 * k - 5.497006427196366) +
1227
- y * (-2.5217340131683033 * y - 21.248923337353073 * k + 17.5119270841813) +
1228
- k * (-21.86122147463605 * k - 189.48180835922747), dstMax);
1229
- dstData[dst + 1] = toRange(255 +
1230
- c * (8.841041422036149 * c + 60.118027045597366 * m + 6.871425592049007 * y + 31.159100130055922 * k - 79.2970844816548) +
1231
- m * (-15.310361306967817 * m + 17.575251261109482 * y + 131.35250912493976 * k - 190.9453302588951) +
1232
- y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) +
1233
- k * (-20.737325471181034 * k - 187.80453709719578), dstMax);
1234
- dstData[dst + 2] = toRange(255 +
1235
- c * (0.8842522430003296 * c + 8.078677503112928 * m + 30.89978309703729 * y - 0.23883238689178934 * k - 14.183576799673286) +
1236
- m * (10.49593273432072 * m + 63.02378494754052 * y + 50.606957656360734 * k - 112.23884253719248) +
1237
- y * (0.03296041114873217 * y + 115.60384449646641 * k - 193.58209356861505) +
1238
- k * (-22.33816807309886 * k - 180.12613974708367), dstMax);
1268
+ cmykToRgbValues(c, m, y, k, out);
1269
+ dstData[dst] = toRange(out[0], dstMax);
1270
+ dstData[dst + 1] = toRange(out[1], dstMax);
1271
+ dstData[dst + 2] = toRange(out[2], dstMax);
1239
1272
  dstData[dst + 3] = srcMax === dstMax ? a : Math.round(a * dstMax / srcMax);
1240
1273
  }
1241
1274
  }
@@ -1418,8 +1451,8 @@ export function readDataZip(
1418
1451
  }
1419
1452
  }
1420
1453
 
1421
- // decodes single PackBits row, returns number of decoded values
1422
- function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray, p: number, step: number, count: number) {
1454
+ // decodes single PackBits row into byte buffer, returns number of decoded bytes
1455
+ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: Uint8Array, count: number) {
1423
1456
  let x = 0;
1424
1457
 
1425
1458
  while (i < end && x < count) {
@@ -1431,19 +1464,15 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
1431
1464
  const value = src[i++];
1432
1465
  let n = 257 - header;
1433
1466
  if (n > count - x) n = count - x;
1467
+ dst.fill(value, x, x + n);
1434
1468
  x += n;
1435
-
1436
- for (; n > 0; n--, p += step) {
1437
- dst[p] = value;
1438
- }
1439
1469
  } else if (header < 128) {
1440
1470
  let n = header + 1;
1441
1471
  if (n > count - x) n = count - x;
1442
1472
  if (n > end - i) n = end - i;
1443
- x += n;
1444
1473
 
1445
- for (; n > 0; n--, p += step) {
1446
- dst[p] = src[i++];
1474
+ for (; n > 0; n--) {
1475
+ dst[x++] = src[i++];
1447
1476
  }
1448
1477
  } else {
1449
1478
  // ignore 128
@@ -1453,6 +1482,51 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
1453
1482
  return x;
1454
1483
  }
1455
1484
 
1485
+ // decodes RLE rows of single 8bit channel, writing values to `data` with `step`
1486
+ // (kept as single function with the loops, inlining row decoder into readDataRLE makes V8 generate much slower code)
1487
+ function decodeRLEChannel8(
1488
+ src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
1489
+ data: PixelArray, offset: number, step: number
1490
+ ) {
1491
+ const srcEnd = src.length | 0;
1492
+
1493
+ for (let y = 0; y < height; y++, li++) {
1494
+ let i = (pos < srcEnd ? pos : srcEnd) | 0;
1495
+ pos += lengths[li];
1496
+ const end = (pos < srcEnd ? pos : srcEnd) | 0;
1497
+ let p = (offset + y * width * step) | 0;
1498
+ let x = 0;
1499
+
1500
+ while (i < end && x < width) {
1501
+ const header = src[i++] | 0;
1502
+
1503
+ if (header > 128) {
1504
+ if (i >= end) break;
1505
+
1506
+ const value = src[i++] | 0;
1507
+ let n = (257 - header) | 0;
1508
+ if (n > width - x) n = (width - x) | 0;
1509
+ x = (x + n) | 0;
1510
+
1511
+ for (const pe = (p + n * step) | 0; p < pe; p = (p + step) | 0) {
1512
+ data[p] = value;
1513
+ }
1514
+ } else if (header < 128) {
1515
+ let n = (header + 1) | 0;
1516
+ if (n > width - x) n = (width - x) | 0;
1517
+ if (n > end - i) n = (end - i) | 0;
1518
+ x = (x + n) | 0;
1519
+
1520
+ for (const ie = (i + n) | 0; i < ie; i = (i + 1) | 0, p = (p + step) | 0) {
1521
+ data[p] = src[i];
1522
+ }
1523
+ } else {
1524
+ // ignore 128
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+
1456
1530
  // converts big-endian 16/32-bit values from bytes and writes them to `data` starting at `p` with `step`
1457
1531
  function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: PixelArray, p: number, step: number) {
1458
1532
  if (bpv === 2) {
@@ -1468,10 +1542,10 @@ function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: Pixe
1468
1542
  }
1469
1543
  }
1470
1544
 
1471
- // decodes RLE rows of single channel, each row starts at the beginning of the image row
1472
- function decodeRLEChannel(
1545
+ // decodes RLE rows of single 16bit or 32bit channel, each row is decoded into `row` buffer and then converted to values
1546
+ function decodeRLEChannelWide(
1473
1547
  src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
1474
- data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array | undefined
1548
+ data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array
1475
1549
  ) {
1476
1550
  const srcEnd = src.length;
1477
1551
  const rowSize = width * bpv;
@@ -1483,12 +1557,8 @@ function decodeRLEChannel(
1483
1557
 
1484
1558
  if (rowStart >= rowEnd) continue;
1485
1559
 
1486
- if (bpv === 1) {
1487
- decodeRLERow(src, rowStart, rowEnd, data, offset + y * width * step, step, width);
1488
- } else {
1489
- const decoded = decodeRLERow(src, rowStart, rowEnd, row!, 0, 1, rowSize);
1490
- scatterValues(row!, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
1491
- }
1560
+ const decoded = decodeRLERow(src, rowStart, rowEnd, row, rowSize);
1561
+ scatterValues(row, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
1492
1562
  }
1493
1563
  }
1494
1564
 
@@ -1496,15 +1566,19 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
1496
1566
  const data = pixelData && pixelData.data;
1497
1567
  const rows = offsets.length * height;
1498
1568
  const lengthSize = large ? 4 : 2;
1569
+ const lengthsMemory = rows * lengthSize;
1570
+ const srcEnd = reader.view.byteLength;
1571
+ const bpv = bytesPerValue(bitDepth); // throws before consuming memory
1572
+ let lengths: Uint16Array | Uint32Array;
1499
1573
 
1500
- consumeMemory(reader, rows * lengthSize);
1574
+ consumeMemory(reader, lengthsMemory);
1501
1575
 
1502
1576
  try {
1503
- if ((reader.offset + rows * lengthSize) > reader.view.byteLength) {
1577
+ if ((reader.offset + lengthsMemory) > srcEnd) {
1504
1578
  throw new Error('RLE row lengths exceed buffer length');
1505
1579
  }
1506
1580
 
1507
- const lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
1581
+ lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
1508
1582
  let total = 0;
1509
1583
 
1510
1584
  if (large) {
@@ -1513,36 +1587,39 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
1513
1587
  for (let i = 0; i < rows; i++) total += (lengths[i] = readUint16(reader));
1514
1588
  }
1515
1589
 
1516
- const start = reader.offset;
1517
- const srcEnd = reader.view.byteLength;
1518
-
1519
- if (total > (srcEnd - start)) {
1590
+ if (total > (srcEnd - reader.offset)) {
1520
1591
  // broken or truncated file, decode only data that is available
1521
1592
  warnOrThrow(reader, 'RLE data exceeds buffer length');
1522
1593
  }
1523
1594
 
1524
- const bpv = bytesPerValue(bitDepth);
1525
1595
  if (data) verifyArrayType(data, bpv);
1596
+ } catch (e) {
1597
+ recoverMemory(reader, lengthsMemory);
1598
+ throw e;
1599
+ }
1526
1600
 
1527
- const src = new Uint8Array(reader.view.buffer, reader.view.byteOffset, srcEnd);
1528
- const row = (data && bpv > 1) ? new Uint8Array(width * bpv) : undefined;
1529
- let pos = start;
1601
+ // decoding can't throw, it's kept outside of try block because V8 generates much slower code for loops inlined into try block
1602
+ const src = new Uint8Array(reader.view.buffer, reader.view.byteOffset, srcEnd);
1603
+ const row = (data && bpv > 1) ? new Uint8Array(width * bpv) : undefined;
1604
+ let pos = reader.offset;
1530
1605
 
1531
- for (let c = 0, li = 0; c < offsets.length; c++, li += height) {
1532
- const offset = offsets[c] | 0;
1533
- const channelStart = pos;
1606
+ for (let c = 0, li = 0; c < offsets.length; c++, li += height) {
1607
+ const offset = offsets[c] | 0;
1608
+ const channelStart = pos;
1534
1609
 
1535
- for (let y = 0; y < height; y++) pos += lengths[li + y];
1610
+ for (let y = 0; y < height; y++) pos += lengths[li + y];
1536
1611
 
1537
- if (data && offset >= 0 && offset < step) {
1538
- decodeRLEChannel(src, lengths, li, channelStart, width, height, data, offset, step, bpv, row);
1612
+ if (data && offset >= 0 && offset < step) {
1613
+ if (bpv === 1) {
1614
+ decodeRLEChannel8(src, lengths, li, channelStart, width, height, data, offset, step);
1615
+ } else {
1616
+ decodeRLEChannelWide(src, lengths, li, channelStart, width, height, data, offset, step, bpv, row!);
1539
1617
  }
1540
1618
  }
1541
-
1542
- reader.offset = pos;
1543
- } finally {
1544
- recoverMemory(reader, rows * lengthSize);
1545
1619
  }
1620
+
1621
+ reader.offset = pos;
1622
+ recoverMemory(reader, lengthsMemory);
1546
1623
  }
1547
1624
 
1548
1625
  export function readSection<T>(
@@ -1690,21 +1767,15 @@ export function readPattern(reader: PsdReader): PatternInfo {
1690
1767
  if (channelsCount > 64) throw new Error(`Invalid pattern channel count: ${channelsCount}`);
1691
1768
  // channels cover whole pattern, each 2 bytes of RLE data can produce at most 128 bytes
1692
1769
  if (width * height > (Math.min(end, reader.view.byteLength) - reader.offset) * 64) throw new Error('Pattern data out of bounds');
1693
- const size = width * height * 4;
1694
- consumeMemory(reader, size);
1695
- const data = new Uint8Array(size);
1696
-
1697
- for (let i = 3; i < data.byteLength; i += 4) {
1698
- data[i] = 255;
1699
- }
1700
1770
 
1701
1771
  // first written arrays are color channels, next one is transparency (Photoshop stores it in the last, sheet mask, array)
1702
1772
  const colorChannels = colorMode === ColorMode.RGB ? 3 : (colorMode === ColorMode.CMYK ? 4 : 1);
1703
- // CMYK channels are decoded into separate buffer (5 values per pixel, inverted, 0 = 100% ink) and converted to RGB at the end
1704
- const cmyk = colorMode === ColorMode.CMYK ? new Uint8Array(width * height * 5).fill(255) : undefined;
1705
- if (cmyk) consumeMemory(reader, cmyk.byteLength);
1706
- const alphaTarget = cmyk ? 4 : 3;
1773
+ const isCmyk = colorMode === ColorMode.CMYK;
1774
+ const alphaTarget = isCmyk ? 4 : 3;
1775
+ const channels: { target: number; depth: number; compressionMode: number; cdata: Uint8Array; ctop: number; cleft: number; w: number; h: number; rowBytes: number; channelSize: number; }[] = [];
1776
+ let maxDecodeSize = 0;
1707
1777
 
1778
+ // validate all channels before allocating any image data
1708
1779
  for (let i = 0, ch = 0; i < (channelsCount + 2) && reader.offset + 4 <= end; i++) {
1709
1780
  const has = readUint32(reader);
1710
1781
  if (!has) continue;
@@ -1738,72 +1809,119 @@ export function readPattern(reader: PsdReader): PatternInfo {
1738
1809
  const h = cbottom - ctop;
1739
1810
  const rowBytes = w * (depth / 8);
1740
1811
  const channelSize = rowBytes * h;
1741
- let bytes: Uint8Array;
1742
1812
 
1743
1813
  if (compressionMode === 0) {
1744
1814
  if (cdata.byteLength < channelSize) throw new Error('Pattern channel data out of bounds');
1745
- bytes = cdata;
1746
1815
  } else if (compressionMode === 1) {
1747
1816
  // each 2 bytes of RLE data can produce at most 128 bytes
1748
1817
  if (channelSize > cdata.byteLength * 64) throw new Error('Pattern channel data out of bounds');
1818
+ maxDecodeSize = Math.max(maxDecodeSize, channelSize);
1819
+ } else {
1820
+ throw new Error('Invalid pattern compression mode');
1821
+ }
1822
+
1823
+ channels.push({ target, depth, compressionMode, cdata, ctop, cleft, w, h, rowBytes, channelSize });
1824
+ }
1825
+
1826
+ // check peak memory up front (image data, CMYK buffer and largest RLE decode buffer),
1827
+ // image data is consumed only after successful decoding so failed patterns don't reduce the limit
1828
+ const size = width * height * 4;
1829
+ const cmykSize = isCmyk ? width * height * 5 : 0;
1830
+ consumeMemory(reader, size + cmykSize + maxDecodeSize);
1831
+ recoverMemory(reader, size + cmykSize + maxDecodeSize);
1832
+
1833
+ const data = new Uint8Array(size);
1834
+
1835
+ for (let i = 3; i < data.byteLength; i += 4) {
1836
+ data[i] = 255;
1837
+ }
1749
1838
 
1750
- consumeMemory(reader, channelSize);
1839
+ // CMYK channels are decoded into separate buffer (5 values per pixel, inverted, 0 = 100% ink) and converted to RGB at the end
1840
+ const cmyk = isCmyk ? new Uint8Array(cmykSize).fill(255) : undefined;
1841
+
1842
+ for (const { target, depth, compressionMode, cdata, ctop, cleft, w, h, rowBytes, channelSize } of channels) {
1843
+ let bytes = cdata;
1844
+
1845
+ if (compressionMode === 1) {
1751
1846
  bytes = new Uint8Array(channelSize);
1752
1847
  const cdataReader = createReader(cdata.buffer, cdata.byteOffset, cdata.byteLength);
1753
1848
  readDataRLE(cdataReader, { data: bytes, width: rowBytes, height: h }, rowBytes, h, 8, 1, [0], false);
1754
- } else {
1755
- throw new Error('Invalid pattern compression mode');
1756
1849
  }
1757
1850
 
1851
+ if (depth !== 8) bytes = patternChannelTo8bit(bytes, w * h, depth);
1852
+
1758
1853
  const ox = cleft - left;
1759
1854
  const oy = ctop - top;
1760
1855
 
1761
- for (let y = 0; y < h; y++) {
1762
- for (let x = 0; x < w; x++) {
1763
- const src = x + y * w;
1764
- const dst = (ox + x + (y + oy) * width) * 4;
1765
- const value = depth === 8 ? bytes[src] : patternValueTo8bit(bytes, src, depth);
1766
-
1767
- if (cmyk) {
1768
- cmyk[(ox + x + (y + oy) * width) * 5 + target] = value;
1769
- } else if (target === 3) {
1770
- data[dst + 3] = value;
1771
- } else if (colorMode === ColorMode.Indexed) {
1772
- const color = palette[value];
1773
- data[dst + 0] = color.r;
1774
- data[dst + 1] = color.g;
1775
- data[dst + 2] = color.b;
1776
- } else if (colorMode === ColorMode.Grayscale) {
1856
+ if (cmyk) {
1857
+ copyPatternChannel(bytes, cmyk, w, h, (ox + oy * width) * 5 + target, width * 5, 5);
1858
+ } else if (target === 3 || colorMode === ColorMode.RGB) {
1859
+ copyPatternChannel(bytes, data, w, h, (ox + oy * width) * 4 + target, width * 4, 4);
1860
+ } else if (colorMode === ColorMode.Indexed) {
1861
+ const colors = new Uint8Array(256 * 3);
1862
+
1863
+ for (let i = 0; i < 256; i++) {
1864
+ colors[i * 3 + 0] = palette[i].r;
1865
+ colors[i * 3 + 1] = palette[i].g;
1866
+ colors[i * 3 + 2] = palette[i].b;
1867
+ }
1868
+
1869
+ for (let y = 0; y < h; y++) {
1870
+ for (let x = 0, src = y * w, dst = (ox + (y + oy) * width) * 4; x < w; x++, src++, dst += 4) {
1871
+ const index = bytes[src] * 3;
1872
+ data[dst + 0] = colors[index + 0];
1873
+ data[dst + 1] = colors[index + 1];
1874
+ data[dst + 2] = colors[index + 2];
1875
+ }
1876
+ }
1877
+ } else { // grayscale
1878
+ for (let y = 0; y < h; y++) {
1879
+ for (let x = 0, src = y * w, dst = (ox + (y + oy) * width) * 4; x < w; x++, src++, dst += 4) {
1880
+ const value = bytes[src];
1777
1881
  data[dst + 0] = value;
1778
1882
  data[dst + 1] = value;
1779
1883
  data[dst + 2] = value;
1780
- } else {
1781
- data[dst + target] = value;
1782
1884
  }
1783
1885
  }
1784
1886
  }
1785
-
1786
- if (compressionMode === 1) recoverMemory(reader, channelSize);
1787
1887
  }
1788
1888
 
1789
1889
  if (cmyk) {
1790
1890
  cmykToRgb({ width, height, data: cmyk }, { width, height, data }, false);
1791
- recoverMemory(reader, cmyk.byteLength);
1792
1891
  }
1793
1892
 
1893
+ consumeMemory(reader, size);
1794
1894
  reader.offset = end;
1795
1895
 
1796
1896
  return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data };
1797
1897
  }
1798
1898
 
1799
- // converts big-endian 16bit or 32bit (float) value at given index to 8bit value
1800
- function patternValueTo8bit(bytes: Uint8Array, index: number, depth: number) {
1899
+ // converts big-endian 16bit or 32bit (float) values to 8bit values
1900
+ function patternChannelTo8bit(bytes: Uint8Array, count: number, depth: number) {
1901
+ const result = new Uint8Array(count);
1902
+
1801
1903
  if (depth === 16) {
1802
- return Math.round(((bytes[index * 2] << 8) | bytes[index * 2 + 1]) / 257);
1904
+ for (let i = 0; i < count; i++) {
1905
+ result[i] = Math.round(((bytes[i * 2] << 8) | bytes[i * 2 + 1]) / 257);
1906
+ }
1803
1907
  } else {
1804
- const view = new DataView(bytes.buffer, bytes.byteOffset + index * 4, 4);
1805
- const value = view.getFloat32(0, false);
1806
- return value > 0 ? (value < 1 ? Math.round(value * 255) : 255) : 0;
1908
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1909
+
1910
+ for (let i = 0; i < count; i++) {
1911
+ const value = view.getFloat32(i * 4, false);
1912
+ result[i] = value > 0 ? (value < 1 ? Math.round(value * 255) : 255) : 0;
1913
+ }
1914
+ }
1915
+
1916
+ return result;
1917
+ }
1918
+
1919
+ // copies w x h channel values into interleaved data starting at `offset`
1920
+ function copyPatternChannel(bytes: Uint8Array, data: Uint8Array, w: number, h: number, offset: number, stride: number, step: number) {
1921
+ for (let y = 0, src = 0; y < h; y++) {
1922
+ for (let x = 0, dst = offset + y * stride; x < w; x++, src++, dst += step) {
1923
+ data[dst] = bytes[src];
1924
+ }
1807
1925
  }
1808
1926
  }
1809
1927