ag-psd 31.0.3 → 33.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 (159) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/00d0e0b4 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/0cc76730 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/20729831 +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/214a593b +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/25f8b84a +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/27518289 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/330ef2d9 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/37ff16b8 +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/3f72a97c +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/75efccd4 +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/762f9305 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/7bcfc50a +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  22. package/.v8-cache/v24.7.0-x64-ef5a0af0/825e9a05 +0 -0
  23. package/.v8-cache/v24.7.0-x64-ef5a0af0/87372817 +0 -0
  24. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  25. package/.v8-cache/v24.7.0-x64-ef5a0af0/891a7c73 +0 -0
  26. package/.v8-cache/v24.7.0-x64-ef5a0af0/8b68a3a2 +0 -0
  27. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  28. package/.v8-cache/v24.7.0-x64-ef5a0af0/a1d12a93 +0 -0
  29. package/.v8-cache/v24.7.0-x64-ef5a0af0/a7e80865 +0 -0
  30. package/.v8-cache/v24.7.0-x64-ef5a0af0/ac317c9c +0 -0
  31. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  32. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  33. package/.v8-cache/v24.7.0-x64-ef5a0af0/b8569d6b +0 -0
  34. package/.v8-cache/v24.7.0-x64-ef5a0af0/b8b073c4 +0 -0
  35. package/.v8-cache/v24.7.0-x64-ef5a0af0/bb489c89 +0 -0
  36. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  37. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  38. package/.v8-cache/v24.7.0-x64-ef5a0af0/c46725e1 +0 -0
  39. package/.v8-cache/v24.7.0-x64-ef5a0af0/c794cb34 +0 -0
  40. package/.v8-cache/v24.7.0-x64-ef5a0af0/cd4ae980 +0 -0
  41. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  42. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  43. package/.v8-cache/v24.7.0-x64-ef5a0af0/d02dd6f7 +0 -0
  44. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  45. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  46. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6735171 +0 -0
  47. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  48. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  49. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  50. package/.v8-cache/v24.7.0-x64-ef5a0af0/f5417276 +0 -0
  51. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  52. package/.v8-cache/v24.7.0-x64-ef5a0af0/f74d3518 +0 -0
  53. package/.v8-cache/v24.7.0-x64-ef5a0af0/f886deac +0 -0
  54. package/.v8-cache/v24.7.0-x64-ef5a0af0/fd0a2987 +0 -0
  55. package/.v8-cache/v24.7.0-x64-ef5a0af0/fe349423 +0 -0
  56. package/CHANGELOG.md +107 -0
  57. package/LICENSE +5 -0
  58. package/README.md +3 -6
  59. package/README_PSD.md +242 -81
  60. package/dist/abr.d.ts +11 -4
  61. package/dist/abr.js +363 -229
  62. package/dist/abr.js.map +1 -1
  63. package/dist/additionalInfo.d.ts +56 -2
  64. package/dist/additionalInfo.js +600 -364
  65. package/dist/additionalInfo.js.map +1 -1
  66. package/dist/ase.d.ts +6 -1
  67. package/dist/ase.js +61 -61
  68. package/dist/ase.js.map +1 -1
  69. package/dist/bundle.js +3096 -1541
  70. package/dist/descriptor.d.ts +25 -6
  71. package/dist/descriptor.js +380 -135
  72. package/dist/descriptor.js.map +1 -1
  73. package/dist/effectsHelpers.js +12 -12
  74. package/dist/effectsHelpers.js.map +1 -1
  75. package/dist/engineData.d.ts +3 -1
  76. package/dist/engineData.js +34 -13
  77. package/dist/engineData.js.map +1 -1
  78. package/dist/engineData2.js +50 -21
  79. package/dist/engineData2.js.map +1 -1
  80. package/dist/helpers.d.ts +6 -3
  81. package/dist/helpers.js +22 -6
  82. package/dist/helpers.js.map +1 -1
  83. package/dist/imageResources.d.ts +15 -3
  84. package/dist/imageResources.js +529 -144
  85. package/dist/imageResources.js.map +1 -1
  86. package/dist/index.js +1 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/jpeg.js +30 -10
  89. package/dist/jpeg.js.map +1 -1
  90. package/dist/psd.d.ts +154 -18
  91. package/dist/psd.js.map +1 -1
  92. package/dist/psdReader.d.ts +12 -2
  93. package/dist/psdReader.js +720 -421
  94. package/dist/psdReader.js.map +1 -1
  95. package/dist/psdWriter.d.ts +3 -0
  96. package/dist/psdWriter.js +167 -130
  97. package/dist/psdWriter.js.map +1 -1
  98. package/dist/text.js +143 -29
  99. package/dist/text.js.map +1 -1
  100. package/dist/utf8.js +36 -18
  101. package/dist/utf8.js.map +1 -1
  102. package/dist-es/abr.d.ts +11 -4
  103. package/dist-es/abr.js +363 -229
  104. package/dist-es/abr.js.map +1 -1
  105. package/dist-es/additionalInfo.d.ts +56 -2
  106. package/dist-es/additionalInfo.js +600 -364
  107. package/dist-es/additionalInfo.js.map +1 -1
  108. package/dist-es/ase.d.ts +6 -1
  109. package/dist-es/ase.js +61 -61
  110. package/dist-es/ase.js.map +1 -1
  111. package/dist-es/descriptor.d.ts +25 -6
  112. package/dist-es/descriptor.js +380 -135
  113. package/dist-es/descriptor.js.map +1 -1
  114. package/dist-es/effectsHelpers.js +12 -12
  115. package/dist-es/effectsHelpers.js.map +1 -1
  116. package/dist-es/engineData.d.ts +3 -1
  117. package/dist-es/engineData.js +34 -13
  118. package/dist-es/engineData.js.map +1 -1
  119. package/dist-es/engineData2.js +50 -21
  120. package/dist-es/engineData2.js.map +1 -1
  121. package/dist-es/helpers.d.ts +6 -3
  122. package/dist-es/helpers.js +22 -6
  123. package/dist-es/helpers.js.map +1 -1
  124. package/dist-es/imageResources.d.ts +15 -3
  125. package/dist-es/imageResources.js +529 -144
  126. package/dist-es/imageResources.js.map +1 -1
  127. package/dist-es/index.js +1 -0
  128. package/dist-es/index.js.map +1 -1
  129. package/dist-es/jpeg.js +30 -10
  130. package/dist-es/jpeg.js.map +1 -1
  131. package/dist-es/psd.d.ts +154 -18
  132. package/dist-es/psd.js.map +1 -1
  133. package/dist-es/psdReader.d.ts +12 -2
  134. package/dist-es/psdReader.js +720 -421
  135. package/dist-es/psdReader.js.map +1 -1
  136. package/dist-es/psdWriter.d.ts +3 -0
  137. package/dist-es/psdWriter.js +167 -130
  138. package/dist-es/psdWriter.js.map +1 -1
  139. package/dist-es/text.js +143 -29
  140. package/dist-es/text.js.map +1 -1
  141. package/dist-es/utf8.js +36 -18
  142. package/dist-es/utf8.js.map +1 -1
  143. package/package.json +2 -2
  144. package/src/abr.ts +380 -232
  145. package/src/additionalInfo.ts +781 -477
  146. package/src/ase.ts +66 -61
  147. package/src/descriptor.ts +386 -142
  148. package/src/effectsHelpers.ts +9 -9
  149. package/src/engineData.ts +36 -13
  150. package/src/engineData2.ts +468 -432
  151. package/src/helpers.ts +23 -5
  152. package/src/imageResources.ts +658 -167
  153. package/src/index.ts +1 -0
  154. package/src/jpeg.ts +27 -12
  155. package/src/psd.ts +158 -30
  156. package/src/psdReader.ts +732 -414
  157. package/src/psdWriter.ts +186 -132
  158. package/src/text.ts +139 -18
  159. package/src/utf8.ts +35 -21
package/src/abr.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BlnM, DescriptorUnitsValue, parseAngle, parsePercent, parseUnitsToNumber, readVersionAndDescriptor } from './descriptor';
2
2
  import { BlendMode, PatternInfo } from './psd';
3
- import { checkSignature, createReader, readBytes, readDataRLE, readInt16, readInt32, readPascalString, readPattern, readSignature, readUint16, readUint32, readUint8, skipBytes } from './psdReader';
3
+ import { checkSignature, consumeMemory, createReader, PsdReader, readBytes, readDataRLE, readInt16, readInt32, readPascalString, readPatternSafe, readSignature, readUint16, readUint32, readUint8, readUnicodeString, skipBytes } from './psdReader';
4
4
 
5
5
  export interface Abr {
6
6
  brushes: Brush[];
@@ -99,6 +99,7 @@ interface DynamicBrushShape {
99
99
 
100
100
  export interface Brush {
101
101
  name: string;
102
+ group?: string[]; // names of brush groups (folders in brushes panel) the brush is in, from outermost to innermost
102
103
  shape: BrushShape;
103
104
  shapeDynamics?: {
104
105
  sizeDynamics: BrushDynamics;
@@ -152,8 +153,8 @@ export interface Brush {
152
153
  transfer?: {
153
154
  flowDynamics: BrushDynamics;
154
155
  opacityDynamics: BrushDynamics;
155
- wetnessDynamics: BrushDynamics;
156
- mixDynamics: BrushDynamics;
156
+ wetnessDynamics?: BrushDynamics; // missing in brushes from older Photoshop versions
157
+ mixDynamics?: BrushDynamics;
157
158
  };
158
159
  brushPose?: {
159
160
  overrideAngle: boolean;
@@ -167,8 +168,8 @@ export interface Brush {
167
168
  };
168
169
  noise: boolean;
169
170
  wetEdges: boolean;
170
- // TODO: build-up
171
- // TODO: smoothing
171
+ buildUp: boolean;
172
+ // smoothing option is stored in toolOptions.smoothing
172
173
  protectTexture?: boolean;
173
174
  spacing: number;
174
175
  brushGroup?: undefined; // ?
@@ -210,7 +211,10 @@ export interface Brush {
210
211
  // internal
211
212
 
212
213
  interface PhryDescriptor {
213
- hierarchy: ({} | {
214
+ hierarchy?: ({
215
+ _classID: 'preset' | 'groupEnd';
216
+ } | {
217
+ _classID: 'Grup';
214
218
  'Nm ': string;
215
219
  zuid: string;
216
220
  })[];
@@ -403,6 +407,9 @@ interface DescDescriptor {
403
407
  }[];
404
408
  }
405
409
 
410
+ // sanity limits for brush sample dimensions (Photoshop brushes are limited to 5000px)
411
+ const MAX_BRUSH_SIZE = 10000;
412
+
406
413
  const toBrushType: { [key: string]: 'brush' | 'mixer brush' | 'smudge brush'; } = {
407
414
  _: 'brush',
408
415
  MixB: 'mixer brush',
@@ -513,262 +520,403 @@ function parseHeightmap(array: Uint8Array) {
513
520
  // return result;
514
521
  }
515
522
 
516
- export function readAbr(buffer: ArrayBufferView, options: { logMissingFeatures?: boolean; } = {}): Abr {
523
+ export interface ReadAbrOptions {
524
+ logMissingFeatures?: boolean;
525
+ /** Throw on unsupported or invalid parts of the file instead of skipping them */
526
+ throwForMissingFeatures?: boolean;
527
+ /** Memory limit for decoded data in bytes (2GB by default), set to `undefined` explicitly to disable */
528
+ totalMemoryLimit?: number;
529
+ }
530
+
531
+ export function readAbr(buffer: ArrayBufferView, options: ReadAbrOptions = {}): Abr {
517
532
  const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength);
533
+ reader.logMissingFeatures = options.logMissingFeatures;
534
+ reader.throwForMissingFeatures = options.throwForMissingFeatures;
535
+ reader.totalMemoryLimit = 'totalMemoryLimit' in options ? options.totalMemoryLimit : 2 * 1024 * 1024 * 1024;
536
+
537
+ // skips invalid or unsupported part of the file
538
+ const skip = (e: unknown, what: string) => {
539
+ if (options.throwForMissingFeatures) throw e;
540
+ options.logMissingFeatures && reader.log(`Skipped ${what}: ${(e as Error).message}`);
541
+ };
542
+
543
+ if (reader.view.byteLength < 4) throw new Error('Invalid ABR file');
544
+
518
545
  const version = readInt16(reader);
519
546
  const samples: SampleInfo[] = [];
520
547
  const brushes: Brush[] = [];
521
548
  const patterns: PatternInfo[] = [];
549
+ const brushByPreset: (Brush | undefined)[] = []; // brush for each preset in 'desc' section (brushes that fail to parse are skipped)
550
+ let hierarchy: PhryDescriptor['hierarchy'];
522
551
 
523
552
  if (version === 1 || version === 2) {
524
- throw new Error(`Unsupported ABR version (${version})`); // TODO: ...
553
+ readLegacyBrushes(reader, version, samples, brushes, skip);
525
554
  } else if (version === 6 || version === 7 || version === 9 || version === 10) {
526
555
  const minorVersion = readInt16(reader);
527
556
  if (minorVersion !== 1 && minorVersion !== 2) throw new Error('Unsupported ABR minor version');
528
557
 
529
- while (reader.offset < reader.view.byteLength) {
558
+ // section header is 12 bytes, ignore any shorter trailing data
559
+ while (reader.offset + 12 <= reader.view.byteLength) {
530
560
  checkSignature(reader, '8BIM');
531
561
  const type = readSignature(reader) as 'samp' | 'desc' | 'patt' | 'phry';
532
- let size = readUint32(reader);
533
- const end = reader.offset + size;
534
-
535
- switch (type) {
536
- case 'samp': {
537
- while (reader.offset < end) {
538
- let brushLength = readUint32(reader);
539
- while (brushLength & 0b11) brushLength++; // pad to 4 byte alignment
540
- const brushEnd = reader.offset + brushLength;
541
-
542
- const id = readPascalString(reader, 1);
543
-
544
- // v1 - Skip the Int16 bounds rectangle and the unknown Int16.
545
- // v2 - Skip the unknown bytes.
546
- skipBytes(reader, minorVersion === 1 ? 10 : 264);
547
-
548
- const y = readInt32(reader);
549
- const x = readInt32(reader);
550
- const h = readInt32(reader) - y;
551
- const w = readInt32(reader) - x;
552
- if (w <= 0 || h <= 0) throw new Error('Invalid bounds');
553
-
554
- const bithDepth = readInt16(reader);
555
- const compression = readUint8(reader); // 0 - raw, 1 - RLE
556
- const alpha = new Uint8Array(w * h);
557
-
558
- if (bithDepth === 8) {
559
- if (compression === 0) {
560
- alpha.set(readBytes(reader, alpha.byteLength));
561
- } else if (compression === 1) {
562
- readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, bithDepth, 1, [0], false);
563
- } else {
564
- throw new Error('Invalid compression');
562
+ const size = readUint32(reader);
563
+ const end = Math.min(reader.offset + size, reader.view.byteLength);
564
+
565
+ try {
566
+ switch (type) {
567
+ case 'samp': {
568
+ while (reader.offset + 4 <= end) {
569
+ let brushLength = readUint32(reader);
570
+ while (brushLength & 0b11) brushLength++; // pad to 4 byte alignment
571
+ const brushEnd = Math.min(reader.offset + brushLength, end);
572
+
573
+ try {
574
+ const sample = readSample(reader, brushEnd, minorVersion);
575
+ samples.push(sample);
576
+ } catch (e) {
577
+ skip(e, 'brush sample');
565
578
  }
566
- } else if (bithDepth === 16) {
567
- if (compression === 0) {
568
- for (let i = 0; i < alpha.byteLength; i++) {
569
- alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
570
- }
571
- } else if (compression === 1) {
572
- throw new Error('not implemented (16bit RLE)'); // TODO: ...
573
- } else {
574
- throw new Error('Invalid compression');
579
+
580
+ reader.offset = brushEnd;
581
+ }
582
+ break;
583
+ }
584
+ case 'desc': {
585
+ const desc: DescDescriptor = readVersionAndDescriptor(reader, true);
586
+
587
+ for (const [index, brush] of (desc.Brsh || []).entries()) {
588
+ try {
589
+ const parsed = parseBrush(brush);
590
+ brushes.push(parsed);
591
+ brushByPreset[index] = parsed;
592
+ } catch (e) {
593
+ skip(e, `brush "${brush['Nm ']}"`);
575
594
  }
576
- } else {
577
- throw new Error('Invalid depth');
578
595
  }
579
-
580
- samples.push({ id, bounds: { x, y, w, h }, alpha });
581
- reader.offset = brushEnd;
596
+ break;
582
597
  }
583
- break;
584
- }
585
- case 'desc': {
586
- const desc: DescDescriptor = readVersionAndDescriptor(reader, true);
587
- // console.log(require('util').inspect(desc, false, 99, true));
588
- // require('fs').writeFileSync('test.log', require('util').inspect(desc, false, 99, false), 'utf8');
589
-
590
- for (const brush of desc.Brsh) {
591
- const b: Brush = {
592
- name: brush['Nm '],
593
- shape: parseBrushShape(brush.Brsh),
594
- spacing: parsePercent(brush.Spcn),
595
- // TODO: brushGroup ???
596
- wetEdges: brush.Wtdg,
597
- noise: brush.Nose,
598
- // TODO: TxtC ??? smoothing / build-up ?
599
- // TODO: 'Rpt ' ???
600
- useBrushSize: brush.useBrushSize, // ???
601
- };
602
-
603
- if (brush.interpretation != null) b.interpretation = brush.interpretation;
604
- if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;
605
-
606
- if (brush.useTipDynamics) {
607
- b.shapeDynamics = {
608
- tiltScale: parsePercent(brush.tiltScale),
609
- sizeDynamics: parseDynamics(brush.szVr),
610
- angleDynamics: parseDynamics(brush.angleDynamics),
611
- roundnessDynamics: parseDynamics(brush.roundnessDynamics),
612
- flipX: brush.flipX,
613
- flipY: brush.flipY,
614
- brushProjection: brush.brushProjection,
615
- minimumDiameter: parsePercent(brush.minimumDiameter),
616
- minimumRoundness: parsePercent(brush.minimumRoundness),
617
- };
598
+ case 'patt': {
599
+ while (reader.offset + 4 <= end) {
600
+ const pattern = readPatternSafe(reader);
601
+ if (pattern) patterns.push(pattern);
618
602
  }
603
+ break;
604
+ }
605
+ case 'phry': { // brush groups (folders in brushes panel)
606
+ // flat list of group starts, presets (in the same order as in 'desc' section) and group ends
607
+ const desc: PhryDescriptor = readVersionAndDescriptor(reader, true);
608
+ hierarchy = desc.hierarchy;
609
+ break;
610
+ }
611
+ default:
612
+ // 'lPdc' (tool presets) and other unknown sections
613
+ options.logMissingFeatures && reader.log(`Unhandled brush section: ${type}`);
614
+ break;
615
+ }
616
+ } catch (e) {
617
+ skip(e, `brush section ${type}`);
618
+ }
619
619
 
620
- if (brush.useScatter) {
621
- b.scatter = {
622
- count: brush['Cnt '],
623
- bothAxes: brush.bothAxes,
624
- countDynamics: parseDynamics(brush.countDynamics),
625
- scatterDynamics: parseDynamics(brush.scatterDynamics),
626
- };
627
- }
620
+ // skip any unread data in the section and align to 4 bytes
621
+ reader.offset = end;
622
+ while (reader.offset % 4) reader.offset++;
623
+ }
624
+ } else {
625
+ throw new Error(`Unsupported ABR version (${version})`);
626
+ }
628
627
 
629
- if (brush.useTexture && brush.Txtr) {
630
- b.texture = {
631
- id: brush.Txtr.Idnt,
632
- name: brush.Txtr['Nm '],
633
- blendMode: BlnM.decode(brush.textureBlendMode),
634
- depth: parsePercent(brush.textureDepth),
635
- depthMinimum: parsePercent(brush.minimumDepth),
636
- depthDynamics: parseDynamics(brush.textureDepthDynamics),
637
- scale: parsePercent(brush.textureScale),
638
- invert: brush.InvT,
639
- brightness: brush.textureBrightness,
640
- contrast: brush.textureContrast,
641
- textureEachTip: !!brush.TxtC,
642
- };
643
- }
628
+ if (hierarchy) {
629
+ const groups: string[] = [];
630
+ let presetIndex = 0;
631
+
632
+ for (const item of hierarchy) {
633
+ if (item._classID === 'Grup') {
634
+ groups.push(item['Nm ']);
635
+ } else if (item._classID === 'groupEnd') {
636
+ groups.pop();
637
+ } else if (item._classID === 'preset') {
638
+ const brush = brushByPreset[presetIndex];
639
+ if (brush && groups.length) brush.group = groups.slice();
640
+ presetIndex++;
641
+ }
642
+ }
643
+ }
644
644
 
645
- const db = brush.dualBrush;
646
- if (db && db.useDualBrush) {
647
- b.dualBrush = {
648
- flip: db.Flip,
649
- shape: parseBrushShape(db.Brsh),
650
- blendMode: BlnM.decode(db.BlnM),
651
- useScatter: db.useScatter,
652
- spacing: parsePercent(db.Spcn),
653
- count: db['Cnt '],
654
- bothAxes: db.bothAxes,
655
- countDynamics: parseDynamics(db.countDynamics),
656
- scatterDynamics: parseDynamics(db.scatterDynamics),
657
- };
658
- }
645
+ return { samples, patterns, brushes };
646
+ }
659
647
 
660
- if (brush.useColorDynamics) {
661
- b.colorDynamics = {
662
- foregroundBackground: parseDynamics(brush.clVr!),
663
- hue: parsePercent(brush['H ']!),
664
- saturation: parsePercent(brush.Strt!),
665
- brightness: parsePercent(brush.Brgh!),
666
- purity: parsePercent(brush.purity!),
667
- perTip: brush.colorDynamicsPerTip!,
668
- };
669
- }
648
+ function readSample(reader: PsdReader, brushEnd: number, minorVersion: number): SampleInfo {
649
+ const id = readPascalString(reader, 1);
670
650
 
671
- if (brush.usePaintDynamics) {
672
- b.transfer = {
673
- flowDynamics: parseDynamics(brush.prVr!),
674
- opacityDynamics: parseDynamics(brush.opVr!),
675
- wetnessDynamics: parseDynamics(brush.wtVr!),
676
- mixDynamics: parseDynamics(brush.mxVr!),
677
- };
678
- }
651
+ // v1 - Skip the Int16 bounds rectangle and the unknown Int16.
652
+ // v2 - Skip the unknown bytes.
653
+ skipBytes(reader, minorVersion === 1 ? 10 : 264);
679
654
 
680
- if (brush.useBrushPose) {
681
- b.brushPose = {
682
- overrideAngle: brush.overridePoseAngle!,
683
- overrideTiltX: brush.overridePoseTiltX!,
684
- overrideTiltY: brush.overridePoseTiltY!,
685
- overridePressure: brush.overridePosePressure!,
686
- pressure: parsePercent(brush.brushPosePressure!),
687
- tiltX: brush.brushPoseTiltX!,
688
- tiltY: brush.brushPoseTiltY!,
689
- angle: brush.brushPoseAngle!,
690
- };
691
- }
655
+ return readSampleBitmap(reader, brushEnd, id);
656
+ }
692
657
 
693
- const to = brush.toolOptions;
694
- if (to) {
695
- b.toolOptions = {
696
- type: toBrushType[to._classID] || 'brush',
697
- brushPreset: to.brushPreset,
698
- flow: to.flow ?? 100,
699
- smooth: to.Smoo ?? 0,
700
- mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
701
- opacity: to.Opct ?? 100,
702
- smoothing: !!to.smoothing,
703
- smoothingValue: to.smoothingValue || 0,
704
- smoothingRadiusMode: !!to.smoothingRadiusMode,
705
- smoothingCatchup: !!to.smoothingCatchup,
706
- smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
707
- smoothingZoomCompensation: !!to.smoothingZoomCompensation,
708
- pressureSmoothing: !!to.pressureSmoothing,
709
- usePressureOverridesSize: !!to.usePressureOverridesSize,
710
- usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
711
- useLegacy: !!to.useLegacy,
712
- };
713
-
714
- if (to.prVr) b.toolOptions.flowDynamics = parseDynamics(to.prVr);
715
- if (to.opVr) b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
716
- if (to.szVr) b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
717
- if ('wetness' in to) b.toolOptions.wetness = to.wetness;
718
- if ('dryness' in to) b.toolOptions.dryness = to.dryness;
719
- if ('mix' in to) b.toolOptions.mix = to.mix;
720
- if ('autoFill' in to) b.toolOptions.autoFill = to.autoFill;
721
- if ('autoClean' in to) b.toolOptions.autoClean = to.autoClean;
722
- if ('loadSolidColorOnly' in to) b.toolOptions.loadSolidColorOnly = to.loadSolidColorOnly;
723
- if ('sampleAllLayers' in to) b.toolOptions.sampleAllLayers = to.sampleAllLayers;
724
- if ('SmdF' in to) b.toolOptions.smudgeFingerPainting = to.SmdF;
725
- if ('SmdS' in to) b.toolOptions.smudgeSampleAllLayers = to.SmdS;
726
- if ('Prs ' in to) b.toolOptions.strength = to['Prs '];
727
- }
658
+ // bounds, bit depth, compression and image data of brush sample
659
+ function readSampleBitmap(reader: PsdReader, brushEnd: number, id: string): SampleInfo {
660
+ const y = readInt32(reader);
661
+ const x = readInt32(reader);
662
+ const h = readInt32(reader) - y;
663
+ const w = readInt32(reader) - x;
664
+ if (w <= 0 || h <= 0) throw new Error('Invalid bounds');
665
+ if (w > MAX_BRUSH_SIZE || h > MAX_BRUSH_SIZE) {
666
+ throw new Error(`Brush sample too large (${w}x${h})`);
667
+ }
728
668
 
729
- brushes.push(b);
730
- }
731
- break;
732
- }
733
- case 'patt': {
734
- while (reader.offset < end) {
735
- patterns.push(readPattern(reader));
736
- }
737
- reader.offset = end;
738
- break;
739
- }
740
- case 'phry': {
741
- // TODO: what is this ?
742
- const desc: PhryDescriptor = readVersionAndDescriptor(reader);
743
- // example:
744
- // hierarchy: [
745
- // {
746
- // 'Nm ': 'PRE_EXPORT ',
747
- // zuid: '965209f2-6f35-9a40-aa43-485684382172'
748
- // },
749
- // {},
750
- // ...
751
- // ]
752
- if (options.logMissingFeatures) {
753
- if (desc.hierarchy?.length) {
754
- // console.log('unhandled phry section', desc);
755
- }
756
- }
757
- break;
758
- }
759
- default:
760
- throw new Error(`Invalid brush type: ${type}`);
761
- }
669
+ const bithDepth = readInt16(reader);
670
+ const compression = readUint8(reader); // 0 - raw, 1 - RLE
671
+ if (bithDepth !== 8 && bithDepth !== 16) throw new Error('Invalid depth');
672
+ if (compression !== 0 && compression !== 1) throw new Error('Invalid compression');
673
+ const rowSize = w * (bithDepth === 16 ? 2 : 1);
674
+ // RLE needs at least 2 bytes for row length and 2 bytes per each 128 byte run
675
+ const minDataSize = compression === 0 ? h * rowSize : h * (2 + Math.ceil(rowSize / 128) * 2);
676
+ if (minDataSize > brushEnd - reader.offset) {
677
+ throw new Error('Brush sample data out of bounds');
678
+ }
679
+
680
+ consumeMemory(reader, w * h * (compression === 1 && bithDepth === 16 ? 3 : 1));
681
+ const alpha = new Uint8Array(w * h);
762
682
 
763
- // align to 4 bytes
764
- while (size % 4) {
765
- reader.offset++;
766
- size++;
683
+ if (bithDepth === 8) {
684
+ if (compression === 0) {
685
+ alpha.set(readBytes(reader, alpha.byteLength));
686
+ } else if (compression === 1) {
687
+ readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, bithDepth, 1, [0], false);
688
+ } else {
689
+ throw new Error('Invalid compression');
690
+ }
691
+ } else if (bithDepth === 16) {
692
+ if (compression === 0) {
693
+ for (let i = 0; i < alpha.byteLength; i++) {
694
+ alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
695
+ }
696
+ } else if (compression === 1) {
697
+ const data = new Uint16Array(w * h);
698
+ readDataRLE(reader, { width: w, height: h, data }, w, h, bithDepth, 1, [0], false);
699
+ for (let i = 0; i < alpha.byteLength; i++) {
700
+ alpha[i] = data[i] >> 8; // convert to 8bit values
767
701
  }
702
+ } else {
703
+ throw new Error('Invalid compression');
768
704
  }
769
705
  } else {
770
- throw new Error(`Unsupported ABR version (${version})`);
706
+ throw new Error('Invalid depth');
771
707
  }
772
708
 
773
- return { samples, patterns, brushes };
709
+ return { id, bounds: { x, y, w, h }, alpha };
710
+ }
711
+
712
+ // ABR versions 1 and 2 (Photoshop 6 and earlier) contain a list of brushes with brush tips in them
713
+ function readLegacyBrushes(reader: PsdReader, version: number, samples: SampleInfo[], brushes: Brush[], skip: (e: unknown, what: string) => void) {
714
+ const count = readUint16(reader);
715
+
716
+ for (let i = 0; i < count; i++) {
717
+ const type = readUint16(reader);
718
+ const size = readUint32(reader);
719
+ const end = reader.offset + size;
720
+
721
+ if (end > reader.view.byteLength) { // truncated file
722
+ skip(new Error('Brush data out of bounds'), `brush ${i + 1}`);
723
+ break;
724
+ }
725
+
726
+ try {
727
+ if (type === 1) { // computed brush
728
+ skipBytes(reader, 4); // misc
729
+ const spacing = readUint16(reader);
730
+ const diameter = readUint16(reader);
731
+ const roundness = readUint16(reader);
732
+ const angle = readInt16(reader);
733
+ const hardness = readUint16(reader);
734
+ if (reader.offset > end) throw new Error('Brush data out of bounds');
735
+
736
+ brushes.push(createLegacyBrush('', {
737
+ type: 'computed',
738
+ size: diameter,
739
+ angle,
740
+ roundness: roundness / 100,
741
+ spacingOn: spacing !== 0,
742
+ spacing: spacing / 100,
743
+ flipX: false,
744
+ flipY: false,
745
+ hardness: hardness / 100,
746
+ }));
747
+ } else if (type === 2) { // sampled brush
748
+ skipBytes(reader, 4); // misc
749
+ const spacing = readUint16(reader);
750
+ const name = version === 2 ? readUnicodeString(reader) : '';
751
+ skipBytes(reader, 1 + 8); // antialiasing, Int16 bounds rectangle (same as Int32 one)
752
+
753
+ // legacy brushes don't have sample ids
754
+ const sample = readSampleBitmap(reader, end, `brush-${i + 1}`);
755
+ samples.push(sample);
756
+ brushes.push(createLegacyBrush(name, {
757
+ type: 'sampled',
758
+ size: Math.max(sample.bounds.w, sample.bounds.h),
759
+ angle: 0,
760
+ roundness: 1,
761
+ spacingOn: spacing !== 0,
762
+ spacing: spacing / 100,
763
+ flipX: false,
764
+ flipY: false,
765
+ name,
766
+ sampledData: sample.id,
767
+ }));
768
+ } else {
769
+ throw new Error(`Unknown brush type: ${type}`);
770
+ }
771
+ } catch (e) {
772
+ skip(e, `brush ${i + 1}`);
773
+ }
774
+
775
+ reader.offset = end;
776
+ }
777
+ }
778
+
779
+ function createLegacyBrush(name: string, shape: BrushShape): Brush {
780
+ return { name, shape, spacing: 1, wetEdges: false, noise: false, buildUp: false, useBrushSize: true };
781
+ }
782
+
783
+ function parseBrush(brush: DescDescriptor['Brsh'][number]): Brush {
784
+ const b: Brush = {
785
+ name: brush['Nm '],
786
+ shape: parseBrushShape(brush.Brsh),
787
+ spacing: parsePercent(brush.Spcn),
788
+ wetEdges: brush.Wtdg,
789
+ noise: brush.Nose,
790
+ buildUp: !!brush['Rpt '],
791
+ useBrushSize: brush.useBrushSize, // ???
792
+ };
793
+
794
+ if (brush.interpretation != null) b.interpretation = brush.interpretation;
795
+ if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;
796
+
797
+ if (brush.useTipDynamics) {
798
+ b.shapeDynamics = {
799
+ tiltScale: parsePercent(brush.tiltScale),
800
+ sizeDynamics: parseDynamics(brush.szVr),
801
+ angleDynamics: parseDynamics(brush.angleDynamics),
802
+ roundnessDynamics: parseDynamics(brush.roundnessDynamics),
803
+ flipX: brush.flipX,
804
+ flipY: brush.flipY,
805
+ brushProjection: brush.brushProjection,
806
+ minimumDiameter: parsePercent(brush.minimumDiameter),
807
+ minimumRoundness: parsePercent(brush.minimumRoundness),
808
+ };
809
+ }
810
+
811
+ if (brush.useScatter) {
812
+ b.scatter = {
813
+ count: brush['Cnt '],
814
+ bothAxes: brush.bothAxes,
815
+ countDynamics: parseDynamics(brush.countDynamics),
816
+ scatterDynamics: parseDynamics(brush.scatterDynamics),
817
+ };
818
+ }
819
+
820
+ if (brush.useTexture && brush.Txtr) {
821
+ b.texture = {
822
+ id: brush.Txtr.Idnt,
823
+ name: brush.Txtr['Nm '],
824
+ blendMode: BlnM.decode(brush.textureBlendMode),
825
+ depth: parsePercent(brush.textureDepth),
826
+ depthMinimum: parsePercent(brush.minimumDepth),
827
+ depthDynamics: parseDynamics(brush.textureDepthDynamics),
828
+ scale: parsePercent(brush.textureScale),
829
+ invert: brush.InvT,
830
+ brightness: brush.textureBrightness,
831
+ contrast: brush.textureContrast,
832
+ textureEachTip: !!brush.TxtC,
833
+ };
834
+ }
835
+
836
+ const db = brush.dualBrush;
837
+ if (db && db.useDualBrush) {
838
+ b.dualBrush = {
839
+ flip: db.Flip,
840
+ shape: parseBrushShape(db.Brsh),
841
+ blendMode: BlnM.decode(db.BlnM),
842
+ useScatter: db.useScatter,
843
+ spacing: parsePercent(db.Spcn),
844
+ count: db['Cnt '],
845
+ bothAxes: db.bothAxes,
846
+ countDynamics: parseDynamics(db.countDynamics),
847
+ scatterDynamics: parseDynamics(db.scatterDynamics),
848
+ };
849
+ }
850
+
851
+ if (brush.useColorDynamics) {
852
+ b.colorDynamics = {
853
+ foregroundBackground: parseDynamics(brush.clVr!),
854
+ hue: parsePercent(brush['H ']!),
855
+ saturation: parsePercent(brush.Strt!),
856
+ brightness: parsePercent(brush.Brgh!),
857
+ purity: parsePercent(brush.purity!),
858
+ perTip: brush.colorDynamicsPerTip!,
859
+ };
860
+ }
861
+
862
+ if (brush.usePaintDynamics) {
863
+ b.transfer = {
864
+ flowDynamics: parseDynamics(brush.prVr!),
865
+ opacityDynamics: parseDynamics(brush.opVr!),
866
+ };
867
+
868
+ if (brush.wtVr) b.transfer.wetnessDynamics = parseDynamics(brush.wtVr);
869
+ if (brush.mxVr) b.transfer.mixDynamics = parseDynamics(brush.mxVr);
870
+ }
871
+
872
+ if (brush.useBrushPose) {
873
+ b.brushPose = {
874
+ overrideAngle: brush.overridePoseAngle!,
875
+ overrideTiltX: brush.overridePoseTiltX!,
876
+ overrideTiltY: brush.overridePoseTiltY!,
877
+ overridePressure: brush.overridePosePressure!,
878
+ pressure: parsePercent(brush.brushPosePressure!),
879
+ tiltX: brush.brushPoseTiltX!,
880
+ tiltY: brush.brushPoseTiltY!,
881
+ angle: brush.brushPoseAngle!,
882
+ };
883
+ }
884
+
885
+ const to = brush.toolOptions;
886
+ if (to) {
887
+ b.toolOptions = {
888
+ type: toBrushType[to._classID] || 'brush',
889
+ brushPreset: to.brushPreset,
890
+ flow: to.flow ?? 100,
891
+ smooth: to.Smoo ?? 0,
892
+ mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
893
+ opacity: to.Opct ?? 100,
894
+ smoothing: !!to.smoothing,
895
+ smoothingValue: to.smoothingValue || 0,
896
+ smoothingRadiusMode: !!to.smoothingRadiusMode,
897
+ smoothingCatchup: !!to.smoothingCatchup,
898
+ smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
899
+ smoothingZoomCompensation: !!to.smoothingZoomCompensation,
900
+ pressureSmoothing: !!to.pressureSmoothing,
901
+ usePressureOverridesSize: !!to.usePressureOverridesSize,
902
+ usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
903
+ useLegacy: !!to.useLegacy,
904
+ };
905
+
906
+ if (to.prVr) b.toolOptions.flowDynamics = parseDynamics(to.prVr);
907
+ if (to.opVr) b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
908
+ if (to.szVr) b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
909
+ if ('wetness' in to) b.toolOptions.wetness = to.wetness;
910
+ if ('dryness' in to) b.toolOptions.dryness = to.dryness;
911
+ if ('mix' in to) b.toolOptions.mix = to.mix;
912
+ if ('autoFill' in to) b.toolOptions.autoFill = to.autoFill;
913
+ if ('autoClean' in to) b.toolOptions.autoClean = to.autoClean;
914
+ if ('loadSolidColorOnly' in to) b.toolOptions.loadSolidColorOnly = to.loadSolidColorOnly;
915
+ if ('sampleAllLayers' in to) b.toolOptions.sampleAllLayers = to.sampleAllLayers;
916
+ if ('SmdF' in to) b.toolOptions.smudgeFingerPainting = to.SmdF;
917
+ if ('SmdS' in to) b.toolOptions.smudgeSampleAllLayers = to.SmdS;
918
+ if ('Prs ' in to) b.toolOptions.strength = to['Prs '];
919
+ }
920
+
921
+ return b;
774
922
  }