ag-psd 32.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 (117) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  22. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  23. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  24. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  25. package/CHANGELOG.md +100 -0
  26. package/LICENSE +5 -0
  27. package/README.md +4 -7
  28. package/README_PSD.md +261 -84
  29. package/TODO.md +13 -84
  30. package/dist/abr.d.ts +11 -4
  31. package/dist/abr.js +362 -230
  32. package/dist/abr.js.map +1 -1
  33. package/dist/additionalInfo.d.ts +56 -2
  34. package/dist/additionalInfo.js +828 -361
  35. package/dist/additionalInfo.js.map +1 -1
  36. package/dist/ase.d.ts +6 -1
  37. package/dist/ase.js +61 -61
  38. package/dist/ase.js.map +1 -1
  39. package/dist/bundle.js +3321 -1364
  40. package/dist/descriptor.d.ts +28 -13
  41. package/dist/descriptor.js +295 -241
  42. package/dist/descriptor.js.map +1 -1
  43. package/dist/helpers.d.ts +7 -1
  44. package/dist/helpers.js +246 -4
  45. package/dist/helpers.js.map +1 -1
  46. package/dist/imageResources.d.ts +15 -3
  47. package/dist/imageResources.js +586 -144
  48. package/dist/imageResources.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/jpeg.js +8 -8
  53. package/dist/jpeg.js.map +1 -1
  54. package/dist/psd.d.ts +292 -19
  55. package/dist/psd.js.map +1 -1
  56. package/dist/psdReader.d.ts +10 -0
  57. package/dist/psdReader.js +424 -216
  58. package/dist/psdReader.js.map +1 -1
  59. package/dist/psdWriter.d.ts +2 -0
  60. package/dist/psdWriter.js +373 -113
  61. package/dist/psdWriter.js.map +1 -1
  62. package/dist/text.d.ts +32 -1
  63. package/dist/text.js +75 -6
  64. package/dist/text.js.map +1 -1
  65. package/dist/utf8.js +0 -3
  66. package/dist/utf8.js.map +1 -1
  67. package/dist-es/abr.d.ts +11 -4
  68. package/dist-es/abr.js +362 -230
  69. package/dist-es/abr.js.map +1 -1
  70. package/dist-es/additionalInfo.d.ts +56 -2
  71. package/dist-es/additionalInfo.js +828 -361
  72. package/dist-es/additionalInfo.js.map +1 -1
  73. package/dist-es/ase.d.ts +6 -1
  74. package/dist-es/ase.js +61 -61
  75. package/dist-es/ase.js.map +1 -1
  76. package/dist-es/descriptor.d.ts +28 -13
  77. package/dist-es/descriptor.js +295 -241
  78. package/dist-es/descriptor.js.map +1 -1
  79. package/dist-es/helpers.d.ts +7 -1
  80. package/dist-es/helpers.js +246 -4
  81. package/dist-es/helpers.js.map +1 -1
  82. package/dist-es/imageResources.d.ts +15 -3
  83. package/dist-es/imageResources.js +586 -144
  84. package/dist-es/imageResources.js.map +1 -1
  85. package/dist-es/index.d.ts +1 -0
  86. package/dist-es/index.js +1 -0
  87. package/dist-es/index.js.map +1 -1
  88. package/dist-es/jpeg.js +8 -8
  89. package/dist-es/jpeg.js.map +1 -1
  90. package/dist-es/psd.d.ts +292 -19
  91. package/dist-es/psd.js.map +1 -1
  92. package/dist-es/psdReader.d.ts +10 -0
  93. package/dist-es/psdReader.js +424 -216
  94. package/dist-es/psdReader.js.map +1 -1
  95. package/dist-es/psdWriter.d.ts +2 -0
  96. package/dist-es/psdWriter.js +373 -113
  97. package/dist-es/psdWriter.js.map +1 -1
  98. package/dist-es/text.d.ts +32 -1
  99. package/dist-es/text.js +75 -6
  100. package/dist-es/text.js.map +1 -1
  101. package/dist-es/utf8.js +0 -3
  102. package/dist-es/utf8.js.map +1 -1
  103. package/package.json +2 -2
  104. package/perf.js +312 -9
  105. package/src/abr.ts +382 -235
  106. package/src/additionalInfo.ts +1071 -482
  107. package/src/ase.ts +66 -61
  108. package/src/descriptor.ts +291 -242
  109. package/src/helpers.ts +266 -3
  110. package/src/imageResources.ts +736 -169
  111. package/src/index.ts +1 -0
  112. package/src/jpeg.ts +8 -8
  113. package/src/psd.ts +271 -32
  114. package/src/psdReader.ts +440 -221
  115. package/src/psdWriter.ts +402 -110
  116. package/src/text.ts +93 -7
  117. package/src/utf8.ts +0 -4
package/src/psdReader.ts CHANGED
@@ -1,7 +1,8 @@
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, RAW_IMAGE_DATA, largeAdditionalInfoKeys, 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
+ import { decodeString } from './utf8';
5
6
  import { InternalImageResources, resourceHandlersMap } from './imageResources';
6
7
 
7
8
  interface ChannelInfo {
@@ -9,7 +10,7 @@ interface ChannelInfo {
9
10
  length: number;
10
11
  }
11
12
 
12
- export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB, ColorMode.Indexed];
13
+ export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB, ColorMode.Indexed, ColorMode.CMYK];
13
14
  const colorModes = ['bitmap', 'grayscale', 'indexed', 'RGB', 'CMYK', '', '', 'multichannel', 'duotone', 'lab'];
14
15
 
15
16
  function setupGrayscale(data: PixelData) {
@@ -27,7 +28,44 @@ export interface PsdReader extends ReadOptions {
27
28
  view: DataView;
28
29
  large: boolean;
29
30
  globalAlpha: boolean;
31
+ layersInGlobalInfo?: boolean; // layers were read from Lr16/Lr32/Layr global additional info
30
32
  log(...args: any[]): void;
33
+ // text decoders, created lazily (null if not supported)
34
+ utf8Decoder?: TextDecoder | null;
35
+ utf8StrictDecoder?: TextDecoder | null;
36
+ gbkDecoder?: TextDecoder | null;
37
+ }
38
+
39
+ function createTextDecoder(label: string, options?: TextDecoderOptions) {
40
+ try {
41
+ return typeof TextDecoder !== 'undefined' ? new TextDecoder(label, options) : null;
42
+ } catch {
43
+ return null; // encoding not supported
44
+ }
45
+ }
46
+
47
+ // non-fatal UTF-8 decoder, malformed byte sequences are replaced with U+FFFD
48
+ export function getUtf8Decoder(reader: PsdReader) {
49
+ if (reader.utf8Decoder === undefined) reader.utf8Decoder = createTextDecoder('utf-8', { ignoreBOM: true });
50
+ return reader.utf8Decoder;
51
+ }
52
+
53
+ // UTF-8 decoder that throws on malformed byte sequences
54
+ export function getUtf8StrictDecoder(reader: PsdReader) {
55
+ if (reader.utf8StrictDecoder === undefined) reader.utf8StrictDecoder = createTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
56
+ return reader.utf8StrictDecoder;
57
+ }
58
+
59
+ // GBK decoder that throws on malformed byte sequences
60
+ export function getGbkStrictDecoder(reader: PsdReader) {
61
+ if (reader.gbkDecoder === undefined) reader.gbkDecoder = createTextDecoder('gbk', { fatal: true });
62
+ return reader.gbkDecoder;
63
+ }
64
+
65
+ export function decodeUtf8(reader: PsdReader, buffer: Uint8Array) {
66
+ // native decoder is faster only for longer strings
67
+ const decoder = buffer.byteLength > 1000 ? getUtf8Decoder(reader) : null;
68
+ return decoder ? decoder.decode(buffer) : decodeString(buffer);
31
69
  }
32
70
 
33
71
  export function createReader(buffer: ArrayBufferLike, offset?: number, length?: number): PsdReader {
@@ -147,6 +185,8 @@ export function readUnicodeString(reader: PsdReader) {
147
185
  }
148
186
 
149
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;
150
190
 
151
191
  function checkReadBounds(reader: PsdReader, size: number) {
152
192
  if (reader.offset < 0 || (reader.offset + size) > reader.view.byteLength) {
@@ -160,10 +200,21 @@ function readUtf16String(reader: PsdReader, length: number, littleEndian: boolea
160
200
  checkReadBounds(reader, length * 2);
161
201
 
162
202
  const view = reader.view;
163
- const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
164
203
  let offset = reader.offset;
165
204
  let text = '';
166
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
+
167
218
  for (let i = 0; i < length; i += STRING_CHUNK_SIZE) {
168
219
  const count = Math.min(STRING_CHUNK_SIZE, length - i);
169
220
 
@@ -203,6 +254,19 @@ export function readUnicodeStringWithLengthLE(reader: PsdReader, length: number)
203
254
  export function readAsciiString(reader: PsdReader, length: number) {
204
255
  if (!(length > 0)) return '';
205
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
+
206
270
  const text = bytesToString(new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length));
207
271
  reader.offset += length;
208
272
  return text;
@@ -222,7 +286,16 @@ export function checkSignature(reader: PsdReader, a: string, b?: string) {
222
286
  }
223
287
 
224
288
  function readShortString(reader: PsdReader, length: number) {
225
- 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;
226
299
  }
227
300
 
228
301
  function isValidSignature(sig: string) {
@@ -244,7 +317,8 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
244
317
  const maxSize = version === 1 ? 30000 : 300000;
245
318
 
246
319
  if (width > maxSize || height > maxSize) throw new Error(`Invalid size: ${width}x${height}`);
247
- if (channels < 1 || channels > 56) throw new Error(`Invalid channel count: ${channels}`);
320
+ // up to 56 channels plus transparency of composite image (Photoshop CS3 writes 57 channels for 53 alpha channels)
321
+ if (channels < 1 || channels > 57) throw new Error(`Invalid channel count: ${channels}`);
248
322
  if (![1, 8, 16, 32].includes(bitsPerChannel)) throw new Error(`Invalid bitsPerChannel: ${bitsPerChannel}`);
249
323
  if (supportedColorModes.indexOf(colorMode) === -1) throw new Error(`Color mode not supported: ${colorModes[colorMode] ?? colorMode}`);
250
324
 
@@ -253,6 +327,7 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
253
327
  Object.assign(reader, readOptions);
254
328
  reader.large = version === 2;
255
329
  reader.globalAlpha = false;
330
+ reader.layersInGlobalInfo = false;
256
331
  if (!('totalMemoryLimit' in reader)) { // setting totalMemoryLimit to undefined explicitly disables memory limit
257
332
  reader.totalMemoryLimit = 2 * 1024 * 1024 * 1024; // default 2GB memory limit
258
333
  }
@@ -280,12 +355,16 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
280
355
  // image resources
281
356
 
282
357
  const imageResources: InternalImageResources = {};
358
+ let unreadResourceBytes = 0;
283
359
 
284
360
  readSection(reader, 1, left => {
285
- while (left() > 0) {
286
- realignWithSignature(reader, isValidSignature);
361
+ // stop if there's not enough data left for resource header (some files have few trailing bytes in this section)
362
+ while (left() >= 12) {
363
+ const signature = realignWithSignature(reader, isValidSignature);
287
364
  const id = readUint16(reader);
288
- readPascalString(reader, 2); // name
365
+ const nameLength = readUint8(reader);
366
+ const name = readBytes(reader, nameLength);
367
+ skipBytes(reader, (nameLength + 1) % 2); // pad to even size
289
368
 
290
369
  readSection(reader, 2, left => {
291
370
  const handler = resourceHandlersMap[id];
@@ -293,20 +372,29 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
293
372
 
294
373
  if (handler && !skip) {
295
374
  try {
296
- handler.read(reader, imageResources, left);
375
+ handler.read(reader, imageResources, left, { signature, id, name, width, height });
297
376
  } catch (e) {
298
377
  if (reader.throwForMissingFeatures) throw e;
299
378
  skipBytes(reader, left());
300
379
  }
301
380
  } else {
302
- // options.logMissingFeatures && console.log(`Unhandled image resource: ${id} (${left()})`);
381
+ reader.logMissingFeatures && reader.log(`Unhandled image resource: ${id} (${left()})`);
303
382
  skipBytes(reader, left());
304
383
  }
305
384
  });
306
385
  }
386
+
387
+ unreadResourceBytes = left();
388
+ skipBytes(reader, left());
307
389
  });
308
390
 
309
- const { layersGroup, layerGroupsEnabledId, ...rest } = imageResources;
391
+ // some files have image resources section length few bytes larger than the data, layer and mask info section
392
+ // starts right after the last resource in that case
393
+ if (unreadResourceBytes && !sectionFits(reader, reader.offset) && sectionFits(reader, reader.offset - unreadResourceBytes)) {
394
+ reader.offset -= unreadResourceBytes;
395
+ }
396
+
397
+ const { layersGroup, layerGroupsEnabledId, savedPathsOrigination, ...rest } = imageResources;
310
398
 
311
399
  if (Object.keys(rest).length) {
312
400
  psd.imageResources = rest;
@@ -354,6 +442,7 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
354
442
  const rawCompositeData = new Uint8Array(reader.view.buffer, reader.view.byteOffset + rawOffset, reader.view.byteLength - rawOffset);
355
443
  psd.rawCompositeData = rawCompositeData;
356
444
  psd.rawCompositeDataInfo = { large: reader.large, globalAlpha: reader.globalAlpha };
445
+ if (reader.layersInGlobalInfo) psd.rawCompositeDataInfo.layersInGlobalInfo = true;
357
446
  } else {
358
447
  const imageData = readImageData(reader, psd);
359
448
  if (reader.useImageData) {
@@ -364,10 +453,6 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
364
453
  }
365
454
  }
366
455
 
367
- // TODO: show converted color mode instead of original PSD file color mode
368
- // but add option to preserve file color mode (need to return image data instead of canvas in that case)
369
- // psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB
370
-
371
456
  return psd;
372
457
  }
373
458
 
@@ -408,6 +493,10 @@ export function readLayerInfo(reader: PsdReader, psd: Psd, imageResources: Inter
408
493
  l.opened = type === SectionDividerType.OpenFolder;
409
494
  l.children = [];
410
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
+
411
500
  if (l.sectionDivider?.key) {
412
501
  l.blendMode = toBlendMode[l.sectionDivider.key] ?? l.blendMode;
413
502
  }
@@ -452,8 +541,11 @@ function readLayerRecord(reader: PsdReader, psd: Psd, imageResources: InternalIm
452
541
 
453
542
  checkSignature(reader, '8BIM');
454
543
  const blendMode = readSignature(reader);
455
- if (!toBlendMode[blendMode]) throw new Error(`Invalid blend mode: '${blendMode}'`);
456
- layer.blendMode = toBlendMode[blendMode];
544
+ if (!toBlendMode[blendMode]) {
545
+ if (reader.throwForMissingFeatures) throw new Error(`Invalid blend mode: '${blendMode}'`);
546
+ reader.logMissingFeatures && reader.log(`Unknown blend mode: '${blendMode}', using normal blend mode`);
547
+ }
548
+ layer.blendMode = toBlendMode[blendMode] || 'normal';
457
549
 
458
550
  layer.opacity = readUint8(reader) / 0xff;
459
551
  layer.clipping = readUint8(reader) === 1;
@@ -487,11 +579,20 @@ function readLayerRecord(reader: PsdReader, psd: Psd, imageResources: InternalIm
487
579
  return { layer, channels };
488
580
  }
489
581
 
582
+ // negative sizes are treated as empty (Photoshop writes them for some empty layers, like 0,0,-1,0 bounds of fill layer)
583
+ // checks if length of section (layer and mask info) at given offset fits in the file
584
+ function sectionFits(reader: PsdReader, offset: number) {
585
+ const lengthSize = reader.large ? 8 : 4;
586
+ if (offset < 0 || offset + lengthSize > reader.view.byteLength) return false;
587
+ const length = reader.large ? reader.view.getUint32(offset) * 0x100000000 + reader.view.getUint32(offset + 4) : reader.view.getUint32(offset);
588
+ return offset + lengthSize + length <= reader.view.byteLength;
589
+ }
590
+
490
591
  function isValidBoxSize(box: { top?: number; left?: number; bottom?: number; right?: number; }, reader: PsdReader) {
491
592
  const width = (box.right || 0) - (box.left || 0);
492
593
  const height = (box.bottom || 0) - (box.top || 0);
493
594
  const maxSize = reader.large ? 300000 : 30000;
494
- return width >= 0 && height >= 0 && width <= maxSize && height <= maxSize;
595
+ return width <= maxSize && height <= maxSize;
495
596
  }
496
597
 
497
598
  function readLayerMaskData(reader: PsdReader, layer: Layer) {
@@ -613,8 +714,8 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
613
714
  let compression = Compression.RawData;
614
715
  let data: Uint8Array | undefined = undefined;
615
716
 
616
- if (channel.length === 1) throw new Error('Invalid channel length');
617
- 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) {
618
719
  compression = readUint16(reader) as Compression;
619
720
  // try to fix broken files where there's 1 byte shift of channel
620
721
  if (compression > 3) {
@@ -622,9 +723,9 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
622
723
  compression = readUint16(reader) as Compression;
623
724
  }
624
725
 
625
- // try to fix broken files where there's 1 byte shift of channel
726
+ // try to fix broken files where there's 1 byte shift of channel (in the other direction)
626
727
  if (compression > 3) {
627
- reader.offset -= 3;
728
+ reader.offset -= 4;
628
729
  compression = readUint16(reader) as Compression;
629
730
  }
630
731
 
@@ -640,7 +741,14 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
640
741
  }
641
742
 
642
743
  if (!reader.useRawData) {
643
- 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
+ }
644
752
  }
645
753
  }
646
754
 
@@ -665,6 +773,7 @@ export function getCompositeImageData(psd: Psd) {
665
773
  if (info) {
666
774
  reader.large = info.large;
667
775
  reader.globalAlpha = info.globalAlpha;
776
+ reader.layersInGlobalInfo = !!info.layersInGlobalInfo;
668
777
  }
669
778
 
670
779
  return readImageData(reader, psd);
@@ -760,18 +869,13 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
760
869
 
761
870
  if (layerWidth && layerHeight && read === LayerDataType.Layer) {
762
871
  if (cmyk) {
763
- if (bitsPerChannel !== 8) throw new Error('bitsPerChannel not supported');
764
- imageData = { width: layerWidth, height: layerHeight, data: new Uint8ClampedArray(layerWidth * layerHeight * 5) } as any as ImageData;
872
+ if (bitsPerChannel !== 8 && bitsPerChannel !== 16) throw new Error('bitsPerChannel not supported');
873
+ imageData = createImageDataBitDepth(layerWidth, layerHeight, bitsPerChannel, 5, memoryLimit);
765
874
  } else {
766
875
  imageData = createImageDataBitDepth(layerWidth, layerHeight, bitsPerChannel, 4, memoryLimit);
767
876
  }
768
877
  }
769
878
 
770
- if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
771
- (layer as any).imageDataRaw = [];
772
- (layer as any).imageDataRawCompression = [];
773
- }
774
-
775
879
  for (const { id, compression, data } of channels) {
776
880
  if (!data) continue;
777
881
 
@@ -794,15 +898,6 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
794
898
  maskData = createImageDataBitDepth(maskWidth, maskHeight, bitsPerChannel, 4, memoryLimit);
795
899
  if (memoryLimit !== undefined) dataReader.totalMemoryLimit = Math.max(0, dataReader.totalMemoryLimit! - maskData.data.byteLength);
796
900
  readData(dataReader, data.byteLength, maskData, compression, maskWidth, maskHeight, bitsPerChannel, 0, large, 4);
797
- if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
798
- if (id === ChannelID.UserMask) {
799
- (layer as any).maskDataRawCompression = compression;
800
- (layer as any).maskDataRaw = data;
801
- } else {
802
- (layer as any).realMaskDataRawCompression = compression;
803
- (layer as any).realMaskDataRaw = data;
804
- }
805
- }
806
901
  setupGrayscale(maskData);
807
902
  resetAlpha(maskData, false);
808
903
  }
@@ -821,12 +916,6 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
821
916
  }
822
917
 
823
918
  readData(dataReader, data.byteLength, targetData, compression, layerWidth, layerHeight, bitsPerChannel, offset, large, cmyk ? 5 : 4);
824
-
825
- if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
826
- (layer as any).imageDataRawCompression[id] = compression;
827
- (layer as any).imageDataRaw[id] = data;
828
- }
829
-
830
919
  }
831
920
 
832
921
  if (id === ChannelID.Transparency) {
@@ -840,7 +929,8 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
840
929
 
841
930
  if (cmyk) {
842
931
  const cmykData = imageData;
843
- imageData = createImageData(cmykData.width, cmykData.height);
932
+ const rgbMemoryLimit = memoryLimit === undefined ? undefined : Math.max(0, memoryLimit - cmykData.data.byteLength);
933
+ imageData = createImageDataBitDepth(cmykData.width, cmykData.height, bitsPerChannel, 4, rgbMemoryLimit);
844
934
  cmykToRgb(cmykData, imageData, false);
845
935
  }
846
936
  }
@@ -880,7 +970,7 @@ export function readGlobalLayerMaskInfo(reader: PsdReader) {
880
970
  const colorSpace2 = readUint16(reader);
881
971
  const colorSpace3 = readUint16(reader);
882
972
  const colorSpace4 = readUint16(reader);
883
- 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)
884
974
  const kind = readUint8(reader);
885
975
  skipBytes(reader, left()); // 3 bytes of padding ?
886
976
  return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind };
@@ -918,7 +1008,16 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition
918
1008
  const key = readSignature(reader);
919
1009
 
920
1010
  // `largeAdditionalInfoKeys` fallback, because some keys don't have 8B64 signature even when they are 64bit
921
- const u64 = sig === '8B64' || (reader.large && largeAdditionalInfoKeys.indexOf(key) !== -1);
1011
+ let u64 = sig === '8B64' || (reader.large && largeAdditionalInfoKeys.indexOf(key) !== -1);
1012
+
1013
+ // these keys were written by older versions of this library with 32bit length and 8BIM signature,
1014
+ // 64bit length always has high 32bits set to 0 (lengths over 4GB are not supported)
1015
+ if (reader.large && sig === '8BIM' && largeAdditionalInfoKeysObserved.indexOf(key) !== -1) {
1016
+ // 32bit length of 0 is followed by signature of the next block, 64bit length would continue with its low 32bits
1017
+ const next = reader.offset + 8 <= reader.view.byteLength ? reader.view.getUint32(reader.offset + 4) : 0;
1018
+ const nextIsSignature = next === 0x3842494d /* 8BIM */ || next === 0x38423634 /* 8B64 */;
1019
+ u64 = reader.view.getUint32(reader.offset) === 0 && !nextIsSignature;
1020
+ }
922
1021
 
923
1022
  readSection(reader, 2, left => {
924
1023
  const handler = infoHandlersMap[key];
@@ -930,7 +1029,7 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition
930
1029
  if (reader.throwForMissingFeatures) throw e;
931
1030
  }
932
1031
  } else {
933
- reader.logMissingFeatures && reader.log(`Unhandled additional info: ${key}`);
1032
+ reader.logMissingFeatures && key !== 'GenI' && key !== 'OCIO' && key !== 'CAI ' && reader.log(`Unhandled additional info: ${key}`);
934
1033
  skipBytes(reader, left());
935
1034
  }
936
1035
 
@@ -996,14 +1095,8 @@ function readImageData(reader: PsdReader, psd: Psd) {
996
1095
  case ColorMode.Grayscale: {
997
1096
  const colorChannels = psd.colorMode === ColorMode.Grayscale ? 1 : 3;
998
1097
  const offsets = compositeChannelOffsets(reader, psd, colorChannels, 3);
999
- const start = reader.offset;
1000
-
1001
1098
  readCompositeChannels(reader, compression, imageData, bitsPerChannel, 4, offsets);
1002
1099
 
1003
- if (RAW_IMAGE_DATA && compression === Compression.RleCompressed) {
1004
- (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
1005
- }
1006
-
1007
1100
  if (psd.colorMode === ColorMode.Grayscale) {
1008
1101
  setupGrayscale(imageData);
1009
1102
  }
@@ -1016,29 +1109,25 @@ function readImageData(reader: PsdReader, psd: Psd) {
1016
1109
  const offsets = compositeChannelOffsets(reader, psd, 1, -1);
1017
1110
  const indexedImageData: PixelData = { width, height, data: new Uint8Array(width * height) };
1018
1111
  readCompositeChannels(reader, compression, indexedImageData, 8, 1, offsets);
1019
- indexedToRgb(indexedImageData, imageData, psd.palette);
1112
+ indexedToRgb(indexedImageData, imageData, psd.palette, psd.imageResources?.transparencyIndex ?? -1);
1020
1113
  break;
1021
1114
  }
1022
1115
  case ColorMode.CMYK: {
1023
- if (bitsPerChannel !== 8) throw new Error('bitsPerChannel not supported');
1116
+ if (bitsPerChannel !== 8 && bitsPerChannel !== 16) throw new Error('bitsPerChannel not supported');
1024
1117
 
1025
1118
  const offsets = compositeChannelOffsets(reader, psd, 4, 4);
1026
- const cmykImageData: PixelData = { width, height, data: new Uint8Array(width * height * 5) };
1027
- const start = reader.offset;
1119
+ const cmykImageData = createImageDataBitDepth(width, height, bitsPerChannel, 5, reader.totalMemoryLimit);
1028
1120
 
1029
- readCompositeChannels(reader, compression, cmykImageData, 8, 5, offsets);
1030
- cmykToRgb(cmykImageData, imageData, true);
1031
-
1032
- if (RAW_IMAGE_DATA && compression === Compression.RleCompressed) {
1033
- (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
1034
- }
1121
+ resetAlpha(cmykImageData, true);
1122
+ readCompositeChannels(reader, compression, cmykImageData, bitsPerChannel, 5, offsets);
1123
+ cmykToRgb(cmykImageData, imageData, !!reader.globalAlpha);
1035
1124
  break;
1036
1125
  }
1037
1126
  default: throw new Error(`Color mode not supported: ${psd.colorMode}`);
1038
1127
  }
1039
1128
 
1040
- // remove weird white matte
1041
- if (reader.globalAlpha) {
1129
+ // remove weird white matte (already removed when converting from CMYK)
1130
+ if (reader.globalAlpha && psd.colorMode !== ColorMode.CMYK) {
1042
1131
  removeWhiteMatte(imageData);
1043
1132
  }
1044
1133
 
@@ -1053,8 +1142,12 @@ function compositeChannelOffsets(reader: PsdReader, psd: Psd, colorChannels: num
1053
1142
  // First extra channel is transparency if document has global alpha (negative layer count). Some non-Photoshop
1054
1143
  // applications write transparency channel without setting global alpha, in that case extra channel is not
1055
1144
  // listed in alpha channel names (Photoshop names all user alpha channels / saved selections).
1056
- const namedChannels = psd.imageResources?.alphaChannelNames?.length ?? 0;
1057
- const hasTransparency = reader.globalAlpha || (channelCount - colorChannels) > namedChannels;
1145
+ // Photoshop also lists merged transparency as first alpha channel named "Transparency" (in documents marked with
1146
+ // Mtrn/Mt16/Mt32 additional info, or in some 16bit documents with layers stored in Lr16 without any other mark)
1147
+ const alphaNames = psd.imageResources?.alphaChannelNames ?? [];
1148
+ const namedChannels = alphaNames.length;
1149
+ const namedTransparency = !!reader.layersInGlobalInfo && alphaNames[0] === 'Transparency' && namedChannels === channelCount - colorChannels;
1150
+ const hasTransparency = reader.globalAlpha || namedTransparency || (channelCount - colorChannels) > namedChannels;
1058
1151
  const offsets: number[] = [];
1059
1152
 
1060
1153
  for (let i = 0; i < channelCount; i++) {
@@ -1138,45 +1231,61 @@ function removeWhiteMatte({ data, width, height }: PixelData) {
1138
1231
  }
1139
1232
  }
1140
1233
 
1141
- function cmykToRgb(cmyk: PixelData, rgb: PixelData, reverseAlpha: boolean) {
1234
+ // converts value in 0-255 range to target range, rounding and clamping it
1235
+ function toRange(value: number, max: number) {
1236
+ value = Math.round(value * (max / 255));
1237
+ return value <= 0 ? 0 : (value >= max ? max : value);
1238
+ }
1239
+
1240
+ // converts CMYK data to RGB using approximation of U.S. Web Coated (SWOP) v2 profile (see cmykToRgbValues)
1241
+ // cmyk - 8 or 16 bit data with 5 channels (cmyka), rgb - 8 or 16 bit data with 4 channels (rgba)
1242
+ // removeMatte - colors are blended with white (no ink), used for composite image of documents with global alpha
1243
+ function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
1142
1244
  const size = rgb.width * rgb.height * 4;
1143
1245
  const srcData = cmyk.data;
1144
1246
  const dstData = rgb.data;
1247
+ const srcMax = srcData instanceof Uint16Array ? 0xffff : 0xff;
1248
+ const dstMax = dstData instanceof Uint16Array ? 0xffff : 0xff;
1249
+ const out = [0, 0, 0];
1145
1250
 
1146
1251
  for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
1147
- const c = srcData[src];
1148
- const m = srcData[src + 1];
1149
- const y = srcData[src + 2];
1150
- const k = srcData[src + 3];
1151
- dstData[dst] = ((((c * k) | 0) / 255) | 0);
1152
- dstData[dst + 1] = ((((m * k) | 0) / 255) | 0);
1153
- dstData[dst + 2] = ((((y * k) | 0) / 255) | 0);
1154
- dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4];
1155
- }
1156
-
1157
- // for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
1158
- // const c = 1 - (srcData[src + 0] / 255);
1159
- // const m = 1 - (srcData[src + 1] / 255);
1160
- // const y = 1 - (srcData[src + 2] / 255);
1161
- // // const k = srcData[src + 3] / 255;
1162
- // dstData[dst + 0] = ((1 - c * 0.8) * 255) | 0;
1163
- // dstData[dst + 1] = ((1 - m * 0.8) * 255) | 0;
1164
- // dstData[dst + 2] = ((1 - y * 0.8) * 255) | 0;
1165
- // dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4];
1166
- // }
1167
- }
1168
-
1169
- function indexedToRgb(indexed: PixelData, rgb: PixelData, palette: RGB[]) {
1252
+ const a = srcData[src + 4];
1253
+
1254
+ // image data stores inverted values (0 = 100% ink)
1255
+ let c = 1 - srcData[src] / srcMax;
1256
+ let m = 1 - srcData[src + 1] / srcMax;
1257
+ let y = 1 - srcData[src + 2] / srcMax;
1258
+ let k = 1 - srcData[src + 3] / srcMax;
1259
+
1260
+ if (removeMatte && a > 0 && a < srcMax) {
1261
+ const ra = srcMax / a;
1262
+ c = Math.min(1, c * ra);
1263
+ m = Math.min(1, m * ra);
1264
+ y = Math.min(1, y * ra);
1265
+ k = Math.min(1, k * ra);
1266
+ }
1267
+
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);
1272
+ dstData[dst + 3] = srcMax === dstMax ? a : Math.round(a * dstMax / srcMax);
1273
+ }
1274
+ }
1275
+
1276
+ // transparencyIndex - palette index of transparent color (-1 if there's none)
1277
+ function indexedToRgb(indexed: PixelData, rgb: PixelData, palette: RGB[], transparencyIndex: number) {
1170
1278
  const size = indexed.width * indexed.height;
1171
1279
  const srcData = indexed.data;
1172
1280
  const dstData = rgb.data;
1173
1281
 
1174
1282
  for (let src = 0, dst = 0; src < size; src++, dst += 4) {
1175
- const c = palette[srcData[src]];
1283
+ const index = srcData[src];
1284
+ const c = palette[index];
1176
1285
  dstData[dst + 0] = c.r;
1177
1286
  dstData[dst + 1] = c.g;
1178
1287
  dstData[dst + 2] = c.b;
1179
- dstData[dst + 3] = 255;
1288
+ dstData[dst + 3] = index === transparencyIndex ? 0 : 255;
1180
1289
  }
1181
1290
  }
1182
1291
 
@@ -1342,8 +1451,8 @@ export function readDataZip(
1342
1451
  }
1343
1452
  }
1344
1453
 
1345
- // decodes single PackBits row, returns number of decoded values
1346
- 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) {
1347
1456
  let x = 0;
1348
1457
 
1349
1458
  while (i < end && x < count) {
@@ -1355,19 +1464,15 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
1355
1464
  const value = src[i++];
1356
1465
  let n = 257 - header;
1357
1466
  if (n > count - x) n = count - x;
1467
+ dst.fill(value, x, x + n);
1358
1468
  x += n;
1359
-
1360
- for (; n > 0; n--, p += step) {
1361
- dst[p] = value;
1362
- }
1363
1469
  } else if (header < 128) {
1364
1470
  let n = header + 1;
1365
1471
  if (n > count - x) n = count - x;
1366
1472
  if (n > end - i) n = end - i;
1367
- x += n;
1368
1473
 
1369
- for (; n > 0; n--, p += step) {
1370
- dst[p] = src[i++];
1474
+ for (; n > 0; n--) {
1475
+ dst[x++] = src[i++];
1371
1476
  }
1372
1477
  } else {
1373
1478
  // ignore 128
@@ -1377,6 +1482,51 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
1377
1482
  return x;
1378
1483
  }
1379
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
+
1380
1530
  // converts big-endian 16/32-bit values from bytes and writes them to `data` starting at `p` with `step`
1381
1531
  function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: PixelArray, p: number, step: number) {
1382
1532
  if (bpv === 2) {
@@ -1392,10 +1542,10 @@ function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: Pixe
1392
1542
  }
1393
1543
  }
1394
1544
 
1395
- // decodes RLE rows of single channel, each row starts at the beginning of the image row
1396
- 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(
1397
1547
  src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
1398
- data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array | undefined
1548
+ data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array
1399
1549
  ) {
1400
1550
  const srcEnd = src.length;
1401
1551
  const rowSize = width * bpv;
@@ -1407,12 +1557,8 @@ function decodeRLEChannel(
1407
1557
 
1408
1558
  if (rowStart >= rowEnd) continue;
1409
1559
 
1410
- if (bpv === 1) {
1411
- decodeRLERow(src, rowStart, rowEnd, data, offset + y * width * step, step, width);
1412
- } else {
1413
- const decoded = decodeRLERow(src, rowStart, rowEnd, row!, 0, 1, rowSize);
1414
- scatterValues(row!, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
1415
- }
1560
+ const decoded = decodeRLERow(src, rowStart, rowEnd, row, rowSize);
1561
+ scatterValues(row, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
1416
1562
  }
1417
1563
  }
1418
1564
 
@@ -1420,15 +1566,19 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
1420
1566
  const data = pixelData && pixelData.data;
1421
1567
  const rows = offsets.length * height;
1422
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;
1423
1573
 
1424
- consumeMemory(reader, rows * lengthSize);
1574
+ consumeMemory(reader, lengthsMemory);
1425
1575
 
1426
1576
  try {
1427
- if ((reader.offset + rows * lengthSize) > reader.view.byteLength) {
1577
+ if ((reader.offset + lengthsMemory) > srcEnd) {
1428
1578
  throw new Error('RLE row lengths exceed buffer length');
1429
1579
  }
1430
1580
 
1431
- const lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
1581
+ lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
1432
1582
  let total = 0;
1433
1583
 
1434
1584
  if (large) {
@@ -1437,36 +1587,39 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
1437
1587
  for (let i = 0; i < rows; i++) total += (lengths[i] = readUint16(reader));
1438
1588
  }
1439
1589
 
1440
- const start = reader.offset;
1441
- const srcEnd = reader.view.byteLength;
1442
-
1443
- if (total > (srcEnd - start)) {
1590
+ if (total > (srcEnd - reader.offset)) {
1444
1591
  // broken or truncated file, decode only data that is available
1445
1592
  warnOrThrow(reader, 'RLE data exceeds buffer length');
1446
1593
  }
1447
1594
 
1448
- const bpv = bytesPerValue(bitDepth);
1449
1595
  if (data) verifyArrayType(data, bpv);
1596
+ } catch (e) {
1597
+ recoverMemory(reader, lengthsMemory);
1598
+ throw e;
1599
+ }
1450
1600
 
1451
- const src = new Uint8Array(reader.view.buffer, reader.view.byteOffset, srcEnd);
1452
- const row = (data && bpv > 1) ? new Uint8Array(width * bpv) : undefined;
1453
- 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;
1454
1605
 
1455
- for (let c = 0, li = 0; c < offsets.length; c++, li += height) {
1456
- const offset = offsets[c] | 0;
1457
- 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;
1458
1609
 
1459
- for (let y = 0; y < height; y++) pos += lengths[li + y];
1610
+ for (let y = 0; y < height; y++) pos += lengths[li + y];
1460
1611
 
1461
- if (data && offset >= 0 && offset < step) {
1462
- 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!);
1463
1617
  }
1464
1618
  }
1465
-
1466
- reader.offset = pos;
1467
- } finally {
1468
- recoverMemory(reader, rows * lengthSize);
1469
1619
  }
1620
+
1621
+ reader.offset = pos;
1622
+ recoverMemory(reader, lengthsMemory);
1470
1623
  }
1471
1624
 
1472
1625
  export function readSection<T>(
@@ -1520,11 +1673,11 @@ export function readColor(reader: PsdReader): Color {
1520
1673
  skipBytes(reader, 2);
1521
1674
  return { h, s, b };
1522
1675
  }
1523
- case ColorSpace.CMYK: {
1524
- const c = readUint16(reader) / 257;
1525
- const m = readUint16(reader) / 257;
1526
- const y = readUint16(reader) / 257;
1527
- const k = readUint16(reader) / 257;
1676
+ case ColorSpace.CMYK: { // 0 = 100% ink
1677
+ const c = (0xffff - readUint16(reader)) / 257;
1678
+ const m = (0xffff - readUint16(reader)) / 257;
1679
+ const y = (0xffff - readUint16(reader)) / 257;
1680
+ const k = (0xffff - readUint16(reader)) / 257;
1528
1681
  return { c, m, y, k };
1529
1682
  }
1530
1683
  case ColorSpace.Lab: {
@@ -1546,6 +1699,24 @@ export function readColor(reader: PsdReader): Color {
1546
1699
  }
1547
1700
  }
1548
1701
 
1702
+ const MAX_PATTERN_SIZE = 30000;
1703
+
1704
+ // reads pattern, unsupported or invalid patterns are skipped (returns undefined) unless throwForMissingFeatures is set
1705
+ export function readPatternSafe(reader: PsdReader): PatternInfo | undefined {
1706
+ let length = reader.view.getUint32(reader.offset);
1707
+ while (length % 4) length++;
1708
+ const end = reader.offset + 4 + length;
1709
+
1710
+ try {
1711
+ return readPattern(reader);
1712
+ } catch (e) {
1713
+ if (reader.throwForMissingFeatures) throw e;
1714
+ reader.logMissingFeatures && reader.log(`Skipped pattern: ${(e as Error).message}`);
1715
+ reader.offset = end;
1716
+ return undefined;
1717
+ }
1718
+ }
1719
+
1549
1720
  export function readPattern(reader: PsdReader): PatternInfo {
1550
1721
  let length = readUint32(reader);
1551
1722
  while (length % 4) length++;
@@ -1557,8 +1728,8 @@ export function readPattern(reader: PsdReader): PatternInfo {
1557
1728
  const x = readInt16(reader);
1558
1729
  const y = readInt16(reader);
1559
1730
 
1560
- // we only support RGB and grayscale for now
1561
- if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed) {
1731
+ // we only support RGB, grayscale, indexed and CMYK for now
1732
+ if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed && colorMode !== ColorMode.CMYK) {
1562
1733
  throw new Error(`Unsupported pattern color mode: ${colorMode}`);
1563
1734
  }
1564
1735
 
@@ -1575,7 +1746,8 @@ export function readPattern(reader: PsdReader): PatternInfo {
1575
1746
  })
1576
1747
  }
1577
1748
 
1578
- skipBytes(reader, 4); // no idea what this is
1749
+ // some writers follow the palette with 4 extra bytes (unknown)
1750
+ if (reader.view.getUint32(reader.offset) !== 3) skipBytes(reader, 4);
1579
1751
  }
1580
1752
 
1581
1753
  // virtual memory array list
@@ -1590,19 +1762,28 @@ export function readPattern(reader: PsdReader): PatternInfo {
1590
1762
  const channelsCount = readUint32(reader);
1591
1763
  const width = right - left;
1592
1764
  const height = bottom - top;
1593
- const size = width * height * 4;
1594
- consumeMemory(reader, size);
1595
- const data = new Uint8Array(size);
1596
-
1597
- for (let i = 3; i < data.byteLength; i += 4) {
1598
- data[i] = 255;
1599
- }
1600
-
1601
- for (let i = 0, ch = 0; i < (channelsCount + 2); i++) {
1765
+ if (width < 0 || height < 0) throw new Error('Invalid pattern bounds');
1766
+ if (width > MAX_PATTERN_SIZE || height > MAX_PATTERN_SIZE) throw new Error(`Pattern too large (${width}x${height})`);
1767
+ if (channelsCount > 64) throw new Error(`Invalid pattern channel count: ${channelsCount}`);
1768
+ // channels cover whole pattern, each 2 bytes of RLE data can produce at most 128 bytes
1769
+ if (width * height > (Math.min(end, reader.view.byteLength) - reader.offset) * 64) throw new Error('Pattern data out of bounds');
1770
+
1771
+ // first written arrays are color channels, next one is transparency (Photoshop stores it in the last, sheet mask, array)
1772
+ const colorChannels = colorMode === ColorMode.RGB ? 3 : (colorMode === ColorMode.CMYK ? 4 : 1);
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;
1777
+
1778
+ // validate all channels before allocating any image data
1779
+ for (let i = 0, ch = 0; i < (channelsCount + 2) && reader.offset + 4 <= end; i++) {
1602
1780
  const has = readUint32(reader);
1603
1781
  if (!has) continue;
1604
1782
 
1605
1783
  const length = readUint32(reader);
1784
+ if (!length) continue;
1785
+ if (length < 23) throw new Error('Invalid pattern channel length');
1786
+
1606
1787
  const pixelDepth = readUint32(reader);
1607
1788
  const ctop = readUint32(reader);
1608
1789
  const cleft = readUint32(reader);
@@ -1610,105 +1791,143 @@ export function readPattern(reader: PsdReader): PatternInfo {
1610
1791
  const cright = readUint32(reader);
1611
1792
  const pixelDepth2 = readUint16(reader);
1612
1793
  const compressionMode = readUint8(reader); // 0 - raw, 1 - rle
1613
- const dataLength = length - (4 + 16 + 2 + 1);
1614
- const cdata = readBytes(reader, dataLength);
1794
+ const cdata = readBytes(reader, length - 23);
1795
+ const depth = (pixelDepth2 === 8 || pixelDepth2 === 16 || pixelDepth2 === 32) ? pixelDepth2 : pixelDepth;
1796
+
1797
+ if (depth !== 8 && depth !== 16 && depth !== 32) throw new Error(`Unsupported pattern pixel depth: ${depth}`);
1615
1798
 
1616
- if (pixelDepth !== 8 || pixelDepth2 !== 8) {
1617
- throw new Error('16bit pixel depth not supported for patterns');
1799
+ if (ctop < top || cleft < left || cbottom > bottom || cright > right || cbottom < ctop || cright < cleft) {
1800
+ throw new Error('Pattern channel bounds outside of pattern bounds');
1618
1801
  }
1619
1802
 
1803
+ const target = ch < colorChannels ? ch : (ch === colorChannels ? alphaTarget : -1);
1804
+ ch++;
1805
+
1806
+ if (target === -1) continue;
1807
+
1620
1808
  const w = cright - cleft;
1621
1809
  const h = cbottom - ctop;
1622
- const ox = cleft - left;
1623
- const oy = ctop - top;
1810
+ const rowBytes = w * (depth / 8);
1811
+ const channelSize = rowBytes * h;
1624
1812
 
1625
1813
  if (compressionMode === 0) {
1626
- if (colorMode === ColorMode.RGB && ch < 3) {
1627
- for (let y = 0; y < h; y++) {
1628
- for (let x = 0; x < w; x++) {
1629
- const src = x + y * w;
1630
- const dst = (ox + x + (y + oy) * width) * 4;
1631
- data[dst + ch] = cdata[src];
1632
- }
1633
- }
1634
- } else if (colorMode === ColorMode.Grayscale && ch < 1) {
1635
- for (let y = 0; y < h; y++) {
1636
- for (let x = 0; x < w; x++) {
1637
- const src = x + y * w;
1638
- const dst = (ox + x + (y + oy) * width) * 4;
1639
- const value = cdata[src];
1640
- data[dst + 0] = value;
1641
- data[dst + 1] = value;
1642
- data[dst + 2] = value;
1643
- }
1644
- }
1645
- } else if (colorMode === ColorMode.Indexed) {
1646
- for (let y = 0; y < h; y++) {
1647
- for (let x = 0; x < w; x++) {
1648
- const src = x + y * w;
1649
- const dst = (ox + x + (y + oy) * width) * 4;
1650
- const index = cdata[src];
1651
- const color = palette[index];
1652
- data[dst + 0] = color.r;
1653
- data[dst + 1] = color.g;
1654
- data[dst + 2] = color.b;
1655
- }
1656
- }
1657
- } else {
1658
- if (reader.throwForMissingFeatures) throw new Error('Invalid color pattern');
1659
- }
1814
+ if (cdata.byteLength < channelSize) throw new Error('Pattern channel data out of bounds');
1660
1815
  } else if (compressionMode === 1) {
1661
- consumeMemory(reader, w * h);
1816
+ // each 2 bytes of RLE data can produce at most 128 bytes
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
+ }
1662
1825
 
1663
- const pixelData: PixelData = { data, width, height };
1664
- const tempData: PixelData = { data: new Uint8Array(w * h), width: w, height: h };
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
+ }
1838
+
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) {
1846
+ bytes = new Uint8Array(channelSize);
1665
1847
  const cdataReader = createReader(cdata.buffer, cdata.byteOffset, cdata.byteLength);
1848
+ readDataRLE(cdataReader, { data: bytes, width: rowBytes, height: h }, rowBytes, h, 8, 1, [0], false);
1849
+ }
1666
1850
 
1667
- if (colorMode === ColorMode.RGB && ch < 3) {
1668
- readDataRLE(cdataReader, tempData, w, h, 8, 1, [0], false);
1669
- copyChannelToRGBA(tempData, pixelData, ox, oy, ch);
1670
- }
1851
+ if (depth !== 8) bytes = patternChannelTo8bit(bytes, w * h, depth);
1671
1852
 
1672
- if (colorMode === ColorMode.Grayscale && ch < 1) {
1673
- readDataRLE(cdataReader, tempData, w, h, 8, 1, [0], false);
1674
- copyChannelToRGBA(tempData, pixelData, ox, oy, 0);
1675
- setupGrayscale(pixelData);
1676
- }
1853
+ const ox = cleft - left;
1854
+ const oy = ctop - top;
1677
1855
 
1678
- if (colorMode === ColorMode.Indexed) {
1679
- // TODO:
1680
- throw new Error('Indexed pattern color mode not implemented');
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;
1681
1867
  }
1682
1868
 
1683
- recoverMemory(reader, w * h);
1684
- } else {
1685
- throw new Error('Invalid pattern compression mode');
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];
1881
+ data[dst + 0] = value;
1882
+ data[dst + 1] = value;
1883
+ data[dst + 2] = value;
1884
+ }
1885
+ }
1686
1886
  }
1887
+ }
1687
1888
 
1688
- ch++;
1889
+ if (cmyk) {
1890
+ cmykToRgb({ width, height, data: cmyk }, { width, height, data }, false);
1689
1891
  }
1690
1892
 
1893
+ consumeMemory(reader, size);
1691
1894
  reader.offset = end;
1692
1895
 
1693
1896
  return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data };
1694
1897
  }
1695
1898
 
1696
- function copyChannelToRGBA(srcData: PixelData, dstData: PixelData, ox: number, oy: number, offset: number) {
1697
- const w = srcData.width;
1698
- const h = srcData.height;
1699
- const width = dstData.width;
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);
1700
1902
 
1701
- for (let y = 0; y < h; y++) {
1702
- for (let x = 0; x < w; x++) {
1703
- const src = x + y * w;
1704
- const dst = (ox + x + (y + oy) * width) * 4;
1705
- const value = srcData.data[src];
1706
- dstData.data[dst + offset] = value;
1903
+ if (depth === 16) {
1904
+ for (let i = 0; i < count; i++) {
1905
+ result[i] = Math.round(((bytes[i * 2] << 8) | bytes[i * 2 + 1]) / 257);
1906
+ }
1907
+ } else {
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];
1707
1924
  }
1708
1925
  }
1709
1926
  }
1710
1927
 
1711
- function consumeMemory(reader: PsdReader, size: number) {
1928
+ export function consumeMemory(reader: PsdReader, size: number) {
1929
+ if (!(size >= 0)) throw new Error('Invalid size');
1930
+
1712
1931
  if (reader.totalMemoryLimit !== undefined) {
1713
1932
  if (reader.totalMemoryLimit < size) throw new Error('Exceeded memory limit');
1714
1933
  reader.totalMemoryLimit -= size;