ag-psd 32.0.0 → 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 (103) 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/932f0770 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  22. package/CHANGELOG.md +78 -0
  23. package/LICENSE +5 -0
  24. package/README.md +3 -6
  25. package/README_PSD.md +242 -81
  26. package/dist/abr.d.ts +11 -4
  27. package/dist/abr.js +357 -230
  28. package/dist/abr.js.map +1 -1
  29. package/dist/additionalInfo.d.ts +54 -2
  30. package/dist/additionalInfo.js +487 -306
  31. package/dist/additionalInfo.js.map +1 -1
  32. package/dist/ase.d.ts +6 -1
  33. package/dist/ase.js +61 -61
  34. package/dist/ase.js.map +1 -1
  35. package/dist/bundle.js +1937 -1115
  36. package/dist/descriptor.d.ts +14 -8
  37. package/dist/descriptor.js +202 -186
  38. package/dist/descriptor.js.map +1 -1
  39. package/dist/helpers.d.ts +1 -1
  40. package/dist/helpers.js +4 -2
  41. package/dist/helpers.js.map +1 -1
  42. package/dist/imageResources.d.ts +15 -3
  43. package/dist/imageResources.js +503 -142
  44. package/dist/imageResources.js.map +1 -1
  45. package/dist/jpeg.js +8 -8
  46. package/dist/jpeg.js.map +1 -1
  47. package/dist/psd.d.ts +148 -17
  48. package/dist/psd.js.map +1 -1
  49. package/dist/psdReader.d.ts +10 -0
  50. package/dist/psdReader.js +278 -166
  51. package/dist/psdReader.js.map +1 -1
  52. package/dist/psdWriter.d.ts +1 -0
  53. package/dist/psdWriter.js +77 -62
  54. package/dist/psdWriter.js.map +1 -1
  55. package/dist/text.js +17 -6
  56. package/dist/text.js.map +1 -1
  57. package/dist/utf8.js +0 -3
  58. package/dist/utf8.js.map +1 -1
  59. package/dist-es/abr.d.ts +11 -4
  60. package/dist-es/abr.js +357 -230
  61. package/dist-es/abr.js.map +1 -1
  62. package/dist-es/additionalInfo.d.ts +54 -2
  63. package/dist-es/additionalInfo.js +487 -306
  64. package/dist-es/additionalInfo.js.map +1 -1
  65. package/dist-es/ase.d.ts +6 -1
  66. package/dist-es/ase.js +61 -61
  67. package/dist-es/ase.js.map +1 -1
  68. package/dist-es/descriptor.d.ts +14 -8
  69. package/dist-es/descriptor.js +202 -186
  70. package/dist-es/descriptor.js.map +1 -1
  71. package/dist-es/helpers.d.ts +1 -1
  72. package/dist-es/helpers.js +4 -2
  73. package/dist-es/helpers.js.map +1 -1
  74. package/dist-es/imageResources.d.ts +15 -3
  75. package/dist-es/imageResources.js +503 -142
  76. package/dist-es/imageResources.js.map +1 -1
  77. package/dist-es/jpeg.js +8 -8
  78. package/dist-es/jpeg.js.map +1 -1
  79. package/dist-es/psd.d.ts +148 -17
  80. package/dist-es/psd.js.map +1 -1
  81. package/dist-es/psdReader.d.ts +10 -0
  82. package/dist-es/psdReader.js +278 -166
  83. package/dist-es/psdReader.js.map +1 -1
  84. package/dist-es/psdWriter.d.ts +1 -0
  85. package/dist-es/psdWriter.js +77 -62
  86. package/dist-es/psdWriter.js.map +1 -1
  87. package/dist-es/text.js +17 -6
  88. package/dist-es/text.js.map +1 -1
  89. package/dist-es/utf8.js +0 -3
  90. package/dist-es/utf8.js.map +1 -1
  91. package/package.json +2 -2
  92. package/src/abr.ts +375 -235
  93. package/src/additionalInfo.ts +663 -420
  94. package/src/ase.ts +66 -61
  95. package/src/descriptor.ts +199 -195
  96. package/src/helpers.ts +4 -1
  97. package/src/imageResources.ts +632 -168
  98. package/src/jpeg.ts +8 -8
  99. package/src/psd.ts +156 -30
  100. package/src/psdReader.ts +273 -172
  101. package/src/psdWriter.ts +95 -58
  102. package/src/text.ts +17 -6
  103. package/src/utf8.ts +0 -4
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
  })[];
@@ -516,258 +520,101 @@ function parseHeightmap(array: Uint8Array) {
516
520
  // return result;
517
521
  }
518
522
 
519
- 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 {
520
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
+
521
545
  const version = readInt16(reader);
522
546
  const samples: SampleInfo[] = [];
523
547
  const brushes: Brush[] = [];
524
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'];
525
551
 
526
552
  if (version === 1 || version === 2) {
527
- throw new Error(`Unsupported ABR version (${version})`); // TODO: ...
553
+ readLegacyBrushes(reader, version, samples, brushes, skip);
528
554
  } else if (version === 6 || version === 7 || version === 9 || version === 10) {
529
555
  const minorVersion = readInt16(reader);
530
556
  if (minorVersion !== 1 && minorVersion !== 2) throw new Error('Unsupported ABR minor version');
531
557
 
532
- 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) {
533
560
  checkSignature(reader, '8BIM');
534
561
  const type = readSignature(reader) as 'samp' | 'desc' | 'patt' | 'phry';
535
562
  const size = readUint32(reader);
536
- const end = reader.offset + size;
537
-
538
- switch (type) {
539
- case 'samp': {
540
- while (reader.offset < end) {
541
- let brushLength = readUint32(reader);
542
- while (brushLength & 0b11) brushLength++; // pad to 4 byte alignment
543
- const brushEnd = reader.offset + brushLength;
544
-
545
- const id = readPascalString(reader, 1);
546
-
547
- // v1 - Skip the Int16 bounds rectangle and the unknown Int16.
548
- // v2 - Skip the unknown bytes.
549
- skipBytes(reader, minorVersion === 1 ? 10 : 264);
550
-
551
- const y = readInt32(reader);
552
- const x = readInt32(reader);
553
- const h = readInt32(reader) - y;
554
- const w = readInt32(reader) - x;
555
- if (w <= 0 || h <= 0) throw new Error('Invalid bounds');
556
- if (w > MAX_BRUSH_SIZE || h > MAX_BRUSH_SIZE) {
557
- throw new Error(`Brush sample too large (${w}x${h})`);
558
- }
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');
578
+ }
559
579
 
560
- const bithDepth = readInt16(reader);
561
- const compression = readUint8(reader); // 0 - raw, 1 - RLE
562
- if (compression === 0 && w * h * (bithDepth === 16 ? 2 : 1) > brushEnd - reader.offset) {
563
- throw new Error('Brush sample data out of bounds');
580
+ reader.offset = brushEnd;
564
581
  }
565
-
566
- const alpha = new Uint8Array(w * h);
567
-
568
- if (bithDepth === 8) {
569
- if (compression === 0) {
570
- alpha.set(readBytes(reader, alpha.byteLength));
571
- } else if (compression === 1) {
572
- readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, bithDepth, 1, [0], false);
573
- } else {
574
- throw new Error('Invalid compression');
575
- }
576
- } else if (bithDepth === 16) {
577
- if (compression === 0) {
578
- for (let i = 0; i < alpha.byteLength; i++) {
579
- alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
580
- }
581
- } else if (compression === 1) {
582
- throw new Error('not implemented (16bit RLE)'); // TODO: ...
583
- } else {
584
- throw new Error('Invalid compression');
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 ']}"`);
585
594
  }
586
- } else {
587
- throw new Error('Invalid depth');
588
595
  }
589
-
590
- samples.push({ id, bounds: { x, y, w, h }, alpha });
591
- reader.offset = brushEnd;
596
+ break;
592
597
  }
593
- break;
594
- }
595
- case 'desc': {
596
- const desc: DescDescriptor = readVersionAndDescriptor(reader, true);
597
- // console.log(require('util').inspect(desc, false, 99, true));
598
- // require('fs').writeFileSync('test.log', require('util').inspect(desc, false, 99, false), 'utf8');
599
-
600
- for (const brush of desc.Brsh) {
601
- const b: Brush = {
602
- name: brush['Nm '],
603
- shape: parseBrushShape(brush.Brsh),
604
- spacing: parsePercent(brush.Spcn),
605
- // TODO: brushGroup ???
606
- wetEdges: brush.Wtdg,
607
- noise: brush.Nose,
608
- // TODO: TxtC ??? smoothing / build-up ?
609
- // TODO: 'Rpt ' ???
610
- useBrushSize: brush.useBrushSize, // ???
611
- };
612
-
613
- if (brush.interpretation != null) b.interpretation = brush.interpretation;
614
- if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;
615
-
616
- if (brush.useTipDynamics) {
617
- b.shapeDynamics = {
618
- tiltScale: parsePercent(brush.tiltScale),
619
- sizeDynamics: parseDynamics(brush.szVr),
620
- angleDynamics: parseDynamics(brush.angleDynamics),
621
- roundnessDynamics: parseDynamics(brush.roundnessDynamics),
622
- flipX: brush.flipX,
623
- flipY: brush.flipY,
624
- brushProjection: brush.brushProjection,
625
- minimumDiameter: parsePercent(brush.minimumDiameter),
626
- minimumRoundness: parsePercent(brush.minimumRoundness),
627
- };
628
- }
629
-
630
- if (brush.useScatter) {
631
- b.scatter = {
632
- count: brush['Cnt '],
633
- bothAxes: brush.bothAxes,
634
- countDynamics: parseDynamics(brush.countDynamics),
635
- scatterDynamics: parseDynamics(brush.scatterDynamics),
636
- };
637
- }
638
-
639
- if (brush.useTexture && brush.Txtr) {
640
- b.texture = {
641
- id: brush.Txtr.Idnt,
642
- name: brush.Txtr['Nm '],
643
- blendMode: BlnM.decode(brush.textureBlendMode),
644
- depth: parsePercent(brush.textureDepth),
645
- depthMinimum: parsePercent(brush.minimumDepth),
646
- depthDynamics: parseDynamics(brush.textureDepthDynamics),
647
- scale: parsePercent(brush.textureScale),
648
- invert: brush.InvT,
649
- brightness: brush.textureBrightness,
650
- contrast: brush.textureContrast,
651
- textureEachTip: !!brush.TxtC,
652
- };
653
- }
654
-
655
- const db = brush.dualBrush;
656
- if (db && db.useDualBrush) {
657
- b.dualBrush = {
658
- flip: db.Flip,
659
- shape: parseBrushShape(db.Brsh),
660
- blendMode: BlnM.decode(db.BlnM),
661
- useScatter: db.useScatter,
662
- spacing: parsePercent(db.Spcn),
663
- count: db['Cnt '],
664
- bothAxes: db.bothAxes,
665
- countDynamics: parseDynamics(db.countDynamics),
666
- scatterDynamics: parseDynamics(db.scatterDynamics),
667
- };
668
- }
669
-
670
- if (brush.useColorDynamics) {
671
- b.colorDynamics = {
672
- foregroundBackground: parseDynamics(brush.clVr!),
673
- hue: parsePercent(brush['H ']!),
674
- saturation: parsePercent(brush.Strt!),
675
- brightness: parsePercent(brush.Brgh!),
676
- purity: parsePercent(brush.purity!),
677
- perTip: brush.colorDynamicsPerTip!,
678
- };
679
- }
680
-
681
- if (brush.usePaintDynamics) {
682
- b.transfer = {
683
- flowDynamics: parseDynamics(brush.prVr!),
684
- opacityDynamics: parseDynamics(brush.opVr!),
685
- wetnessDynamics: parseDynamics(brush.wtVr!),
686
- mixDynamics: parseDynamics(brush.mxVr!),
687
- };
688
- }
689
-
690
- if (brush.useBrushPose) {
691
- b.brushPose = {
692
- overrideAngle: brush.overridePoseAngle!,
693
- overrideTiltX: brush.overridePoseTiltX!,
694
- overrideTiltY: brush.overridePoseTiltY!,
695
- overridePressure: brush.overridePosePressure!,
696
- pressure: parsePercent(brush.brushPosePressure!),
697
- tiltX: brush.brushPoseTiltX!,
698
- tiltY: brush.brushPoseTiltY!,
699
- angle: brush.brushPoseAngle!,
700
- };
701
- }
702
-
703
- const to = brush.toolOptions;
704
- if (to) {
705
- b.toolOptions = {
706
- type: toBrushType[to._classID] || 'brush',
707
- brushPreset: to.brushPreset,
708
- flow: to.flow ?? 100,
709
- smooth: to.Smoo ?? 0,
710
- mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
711
- opacity: to.Opct ?? 100,
712
- smoothing: !!to.smoothing,
713
- smoothingValue: to.smoothingValue || 0,
714
- smoothingRadiusMode: !!to.smoothingRadiusMode,
715
- smoothingCatchup: !!to.smoothingCatchup,
716
- smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
717
- smoothingZoomCompensation: !!to.smoothingZoomCompensation,
718
- pressureSmoothing: !!to.pressureSmoothing,
719
- usePressureOverridesSize: !!to.usePressureOverridesSize,
720
- usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
721
- useLegacy: !!to.useLegacy,
722
- };
723
-
724
- if (to.prVr) b.toolOptions.flowDynamics = parseDynamics(to.prVr);
725
- if (to.opVr) b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
726
- if (to.szVr) b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
727
- if ('wetness' in to) b.toolOptions.wetness = to.wetness;
728
- if ('dryness' in to) b.toolOptions.dryness = to.dryness;
729
- if ('mix' in to) b.toolOptions.mix = to.mix;
730
- if ('autoFill' in to) b.toolOptions.autoFill = to.autoFill;
731
- if ('autoClean' in to) b.toolOptions.autoClean = to.autoClean;
732
- if ('loadSolidColorOnly' in to) b.toolOptions.loadSolidColorOnly = to.loadSolidColorOnly;
733
- if ('sampleAllLayers' in to) b.toolOptions.sampleAllLayers = to.sampleAllLayers;
734
- if ('SmdF' in to) b.toolOptions.smudgeFingerPainting = to.SmdF;
735
- if ('SmdS' in to) b.toolOptions.smudgeSampleAllLayers = to.SmdS;
736
- if ('Prs ' in to) b.toolOptions.strength = to['Prs '];
598
+ case 'patt': {
599
+ while (reader.offset + 4 <= end) {
600
+ const pattern = readPatternSafe(reader);
601
+ if (pattern) patterns.push(pattern);
737
602
  }
738
-
739
- brushes.push(b);
740
- }
741
- break;
742
- }
743
- case 'patt': {
744
- while (reader.offset < end) {
745
- patterns.push(readPattern(reader));
603
+ break;
746
604
  }
747
- reader.offset = end;
748
- break;
749
- }
750
- case 'phry': {
751
- // TODO: what is this ?
752
- const desc: PhryDescriptor = readVersionAndDescriptor(reader);
753
- // example:
754
- // hierarchy: [
755
- // {
756
- // 'Nm ': 'PRE_EXPORT ',
757
- // zuid: '965209f2-6f35-9a40-aa43-485684382172'
758
- // },
759
- // {},
760
- // ...
761
- // ]
762
- if (options.logMissingFeatures) {
763
- if (desc.hierarchy?.length) {
764
- // console.log('unhandled phry section', desc);
765
- }
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;
766
610
  }
767
- break;
611
+ default:
612
+ // 'lPdc' (tool presets) and other unknown sections
613
+ options.logMissingFeatures && reader.log(`Unhandled brush section: ${type}`);
614
+ break;
768
615
  }
769
- default:
770
- throw new Error(`Invalid brush type: ${type}`);
616
+ } catch (e) {
617
+ skip(e, `brush section ${type}`);
771
618
  }
772
619
 
773
620
  // skip any unread data in the section and align to 4 bytes
@@ -778,5 +625,298 @@ export function readAbr(buffer: ArrayBufferView, options: { logMissingFeatures?:
778
625
  throw new Error(`Unsupported ABR version (${version})`);
779
626
  }
780
627
 
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
+
781
645
  return { samples, patterns, brushes };
782
646
  }
647
+
648
+ function readSample(reader: PsdReader, brushEnd: number, minorVersion: number): SampleInfo {
649
+ const id = readPascalString(reader, 1);
650
+
651
+ // v1 - Skip the Int16 bounds rectangle and the unknown Int16.
652
+ // v2 - Skip the unknown bytes.
653
+ skipBytes(reader, minorVersion === 1 ? 10 : 264);
654
+
655
+ return readSampleBitmap(reader, brushEnd, id);
656
+ }
657
+
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
+ }
668
+
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);
682
+
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
701
+ }
702
+ } else {
703
+ throw new Error('Invalid compression');
704
+ }
705
+ } else {
706
+ throw new Error('Invalid depth');
707
+ }
708
+
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;
922
+ }