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.
- package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
- package/CHANGELOG.md +100 -0
- package/LICENSE +5 -0
- package/README.md +4 -7
- package/README_PSD.md +261 -84
- package/TODO.md +13 -84
- package/dist/abr.d.ts +11 -4
- package/dist/abr.js +362 -230
- package/dist/abr.js.map +1 -1
- package/dist/additionalInfo.d.ts +56 -2
- package/dist/additionalInfo.js +828 -361
- package/dist/additionalInfo.js.map +1 -1
- package/dist/ase.d.ts +6 -1
- package/dist/ase.js +61 -61
- package/dist/ase.js.map +1 -1
- package/dist/bundle.js +3321 -1364
- package/dist/descriptor.d.ts +28 -13
- package/dist/descriptor.js +295 -241
- package/dist/descriptor.js.map +1 -1
- package/dist/helpers.d.ts +7 -1
- package/dist/helpers.js +246 -4
- package/dist/helpers.js.map +1 -1
- package/dist/imageResources.d.ts +15 -3
- package/dist/imageResources.js +586 -144
- package/dist/imageResources.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jpeg.js +8 -8
- package/dist/jpeg.js.map +1 -1
- package/dist/psd.d.ts +292 -19
- package/dist/psd.js.map +1 -1
- package/dist/psdReader.d.ts +10 -0
- package/dist/psdReader.js +424 -216
- package/dist/psdReader.js.map +1 -1
- package/dist/psdWriter.d.ts +2 -0
- package/dist/psdWriter.js +373 -113
- package/dist/psdWriter.js.map +1 -1
- package/dist/text.d.ts +32 -1
- package/dist/text.js +75 -6
- package/dist/text.js.map +1 -1
- package/dist/utf8.js +0 -3
- package/dist/utf8.js.map +1 -1
- package/dist-es/abr.d.ts +11 -4
- package/dist-es/abr.js +362 -230
- package/dist-es/abr.js.map +1 -1
- package/dist-es/additionalInfo.d.ts +56 -2
- package/dist-es/additionalInfo.js +828 -361
- package/dist-es/additionalInfo.js.map +1 -1
- package/dist-es/ase.d.ts +6 -1
- package/dist-es/ase.js +61 -61
- package/dist-es/ase.js.map +1 -1
- package/dist-es/descriptor.d.ts +28 -13
- package/dist-es/descriptor.js +295 -241
- package/dist-es/descriptor.js.map +1 -1
- package/dist-es/helpers.d.ts +7 -1
- package/dist-es/helpers.js +246 -4
- package/dist-es/helpers.js.map +1 -1
- package/dist-es/imageResources.d.ts +15 -3
- package/dist-es/imageResources.js +586 -144
- package/dist-es/imageResources.js.map +1 -1
- package/dist-es/index.d.ts +1 -0
- package/dist-es/index.js +1 -0
- package/dist-es/index.js.map +1 -1
- package/dist-es/jpeg.js +8 -8
- package/dist-es/jpeg.js.map +1 -1
- package/dist-es/psd.d.ts +292 -19
- package/dist-es/psd.js.map +1 -1
- package/dist-es/psdReader.d.ts +10 -0
- package/dist-es/psdReader.js +424 -216
- package/dist-es/psdReader.js.map +1 -1
- package/dist-es/psdWriter.d.ts +2 -0
- package/dist-es/psdWriter.js +373 -113
- package/dist-es/psdWriter.js.map +1 -1
- package/dist-es/text.d.ts +32 -1
- package/dist-es/text.js +75 -6
- package/dist-es/text.js.map +1 -1
- package/dist-es/utf8.js +0 -3
- package/dist-es/utf8.js.map +1 -1
- package/package.json +2 -2
- package/perf.js +312 -9
- package/src/abr.ts +382 -235
- package/src/additionalInfo.ts +1071 -482
- package/src/ase.ts +66 -61
- package/src/descriptor.ts +291 -242
- package/src/helpers.ts +266 -3
- package/src/imageResources.ts +736 -169
- package/src/index.ts +1 -0
- package/src/jpeg.ts +8 -8
- package/src/psd.ts +271 -32
- package/src/psdReader.ts +440 -221
- package/src/psdWriter.ts +402 -110
- package/src/text.ts +93 -7
- 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,
|
|
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
|
|
156
|
-
mixDynamics
|
|
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
|
-
|
|
171
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
};
|
|
598
|
+
case 'patt': {
|
|
599
|
+
while (reader.offset + 4 <= end) {
|
|
600
|
+
const pattern = readPatternSafe(reader);
|
|
601
|
+
if (pattern) patterns.push(pattern);
|
|
701
602
|
}
|
|
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 '];
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
brushes.push(b);
|
|
603
|
+
break;
|
|
740
604
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
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
|
-
|
|
611
|
+
default:
|
|
612
|
+
// 'lPdc' (tool presets) and other unknown sections
|
|
613
|
+
options.logMissingFeatures && reader.log(`Unhandled brush section: ${type}`);
|
|
614
|
+
break;
|
|
768
615
|
}
|
|
769
|
-
|
|
770
|
-
|
|
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,305 @@ 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 * (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
|
+
const data = new Uint16Array(w * h);
|
|
693
|
+
|
|
694
|
+
if (compression === 0) {
|
|
695
|
+
for (let i = 0; i < data.length; i++) {
|
|
696
|
+
data[i] = readUint16(reader);
|
|
697
|
+
}
|
|
698
|
+
} else if (compression === 1) {
|
|
699
|
+
readDataRLE(reader, { width: w, height: h, data }, w, h, bithDepth, 1, [0], false);
|
|
700
|
+
} else {
|
|
701
|
+
throw new Error('Invalid compression');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Photoshop uses 0-32768 range for 16bit values internally, use full 0-65535 range only if there are values outside of it
|
|
705
|
+
let max = 0;
|
|
706
|
+
for (let i = 0; i < data.length; i++) max = Math.max(max, data[i]);
|
|
707
|
+
const scale = max > 32768 ? 255 / 65535 : 255 / 32768;
|
|
708
|
+
|
|
709
|
+
for (let i = 0; i < alpha.byteLength; i++) {
|
|
710
|
+
alpha[i] = Math.round(data[i] * scale); // convert to 8bit values
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
throw new Error('Invalid depth');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return { id, bounds: { x, y, w, h }, alpha };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ABR versions 1 and 2 (Photoshop 6 and earlier) contain a list of brushes with brush tips in them
|
|
720
|
+
function readLegacyBrushes(reader: PsdReader, version: number, samples: SampleInfo[], brushes: Brush[], skip: (e: unknown, what: string) => void) {
|
|
721
|
+
const count = readUint16(reader);
|
|
722
|
+
|
|
723
|
+
for (let i = 0; i < count; i++) {
|
|
724
|
+
const type = readUint16(reader);
|
|
725
|
+
const size = readUint32(reader);
|
|
726
|
+
const end = reader.offset + size;
|
|
727
|
+
|
|
728
|
+
if (end > reader.view.byteLength) { // truncated file
|
|
729
|
+
skip(new Error('Brush data out of bounds'), `brush ${i + 1}`);
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
try {
|
|
734
|
+
if (type === 1) { // computed brush
|
|
735
|
+
skipBytes(reader, 4); // misc
|
|
736
|
+
const spacing = readUint16(reader);
|
|
737
|
+
const diameter = readUint16(reader);
|
|
738
|
+
const roundness = readUint16(reader);
|
|
739
|
+
const angle = readInt16(reader);
|
|
740
|
+
const hardness = readUint16(reader);
|
|
741
|
+
if (reader.offset > end) throw new Error('Brush data out of bounds');
|
|
742
|
+
|
|
743
|
+
brushes.push(createLegacyBrush('', {
|
|
744
|
+
type: 'computed',
|
|
745
|
+
size: diameter,
|
|
746
|
+
angle,
|
|
747
|
+
roundness: roundness / 100,
|
|
748
|
+
spacingOn: spacing !== 0,
|
|
749
|
+
spacing: spacing / 100,
|
|
750
|
+
flipX: false,
|
|
751
|
+
flipY: false,
|
|
752
|
+
hardness: hardness / 100,
|
|
753
|
+
}));
|
|
754
|
+
} else if (type === 2) { // sampled brush
|
|
755
|
+
skipBytes(reader, 4); // misc
|
|
756
|
+
const spacing = readUint16(reader);
|
|
757
|
+
const name = version === 2 ? readUnicodeString(reader) : '';
|
|
758
|
+
skipBytes(reader, 1 + 8); // antialiasing, Int16 bounds rectangle (same as Int32 one)
|
|
759
|
+
|
|
760
|
+
// legacy brushes don't have sample ids
|
|
761
|
+
const sample = readSampleBitmap(reader, end, `brush-${i + 1}`);
|
|
762
|
+
samples.push(sample);
|
|
763
|
+
brushes.push(createLegacyBrush(name, {
|
|
764
|
+
type: 'sampled',
|
|
765
|
+
size: Math.max(sample.bounds.w, sample.bounds.h),
|
|
766
|
+
angle: 0,
|
|
767
|
+
roundness: 1,
|
|
768
|
+
spacingOn: spacing !== 0,
|
|
769
|
+
spacing: spacing / 100,
|
|
770
|
+
flipX: false,
|
|
771
|
+
flipY: false,
|
|
772
|
+
name,
|
|
773
|
+
sampledData: sample.id,
|
|
774
|
+
}));
|
|
775
|
+
} else {
|
|
776
|
+
throw new Error(`Unknown brush type: ${type}`);
|
|
777
|
+
}
|
|
778
|
+
} catch (e) {
|
|
779
|
+
skip(e, `brush ${i + 1}`);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
reader.offset = end;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function createLegacyBrush(name: string, shape: BrushShape): Brush {
|
|
787
|
+
return { name, shape, spacing: 1, wetEdges: false, noise: false, buildUp: false, useBrushSize: true };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function parseBrush(brush: DescDescriptor['Brsh'][number]): Brush {
|
|
791
|
+
const b: Brush = {
|
|
792
|
+
name: brush['Nm '],
|
|
793
|
+
shape: parseBrushShape(brush.Brsh),
|
|
794
|
+
spacing: parsePercent(brush.Spcn),
|
|
795
|
+
wetEdges: brush.Wtdg,
|
|
796
|
+
noise: brush.Nose,
|
|
797
|
+
buildUp: !!brush['Rpt '],
|
|
798
|
+
useBrushSize: brush.useBrushSize, // ???
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
if (brush.interpretation != null) b.interpretation = brush.interpretation;
|
|
802
|
+
if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;
|
|
803
|
+
|
|
804
|
+
if (brush.useTipDynamics) {
|
|
805
|
+
b.shapeDynamics = {
|
|
806
|
+
tiltScale: parsePercent(brush.tiltScale),
|
|
807
|
+
sizeDynamics: parseDynamics(brush.szVr),
|
|
808
|
+
angleDynamics: parseDynamics(brush.angleDynamics),
|
|
809
|
+
roundnessDynamics: parseDynamics(brush.roundnessDynamics),
|
|
810
|
+
flipX: brush.flipX,
|
|
811
|
+
flipY: brush.flipY,
|
|
812
|
+
brushProjection: brush.brushProjection,
|
|
813
|
+
minimumDiameter: parsePercent(brush.minimumDiameter),
|
|
814
|
+
minimumRoundness: parsePercent(brush.minimumRoundness),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (brush.useScatter) {
|
|
819
|
+
b.scatter = {
|
|
820
|
+
count: brush['Cnt '],
|
|
821
|
+
bothAxes: brush.bothAxes,
|
|
822
|
+
countDynamics: parseDynamics(brush.countDynamics),
|
|
823
|
+
scatterDynamics: parseDynamics(brush.scatterDynamics),
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (brush.useTexture && brush.Txtr) {
|
|
828
|
+
b.texture = {
|
|
829
|
+
id: brush.Txtr.Idnt,
|
|
830
|
+
name: brush.Txtr['Nm '],
|
|
831
|
+
blendMode: BlnM.decode(brush.textureBlendMode),
|
|
832
|
+
depth: parsePercent(brush.textureDepth),
|
|
833
|
+
depthMinimum: parsePercent(brush.minimumDepth),
|
|
834
|
+
depthDynamics: parseDynamics(brush.textureDepthDynamics),
|
|
835
|
+
scale: parsePercent(brush.textureScale),
|
|
836
|
+
invert: brush.InvT,
|
|
837
|
+
brightness: brush.textureBrightness,
|
|
838
|
+
contrast: brush.textureContrast,
|
|
839
|
+
textureEachTip: !!brush.TxtC,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const db = brush.dualBrush;
|
|
844
|
+
if (db && db.useDualBrush) {
|
|
845
|
+
b.dualBrush = {
|
|
846
|
+
flip: db.Flip,
|
|
847
|
+
shape: parseBrushShape(db.Brsh),
|
|
848
|
+
blendMode: BlnM.decode(db.BlnM),
|
|
849
|
+
useScatter: db.useScatter,
|
|
850
|
+
spacing: parsePercent(db.Spcn),
|
|
851
|
+
count: db['Cnt '],
|
|
852
|
+
bothAxes: db.bothAxes,
|
|
853
|
+
countDynamics: parseDynamics(db.countDynamics),
|
|
854
|
+
scatterDynamics: parseDynamics(db.scatterDynamics),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (brush.useColorDynamics) {
|
|
859
|
+
b.colorDynamics = {
|
|
860
|
+
foregroundBackground: parseDynamics(brush.clVr!),
|
|
861
|
+
hue: parsePercent(brush['H ']!),
|
|
862
|
+
saturation: parsePercent(brush.Strt!),
|
|
863
|
+
brightness: parsePercent(brush.Brgh!),
|
|
864
|
+
purity: parsePercent(brush.purity!),
|
|
865
|
+
perTip: brush.colorDynamicsPerTip!,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (brush.usePaintDynamics) {
|
|
870
|
+
b.transfer = {
|
|
871
|
+
flowDynamics: parseDynamics(brush.prVr!),
|
|
872
|
+
opacityDynamics: parseDynamics(brush.opVr!),
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
if (brush.wtVr) b.transfer.wetnessDynamics = parseDynamics(brush.wtVr);
|
|
876
|
+
if (brush.mxVr) b.transfer.mixDynamics = parseDynamics(brush.mxVr);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (brush.useBrushPose) {
|
|
880
|
+
b.brushPose = {
|
|
881
|
+
overrideAngle: brush.overridePoseAngle!,
|
|
882
|
+
overrideTiltX: brush.overridePoseTiltX!,
|
|
883
|
+
overrideTiltY: brush.overridePoseTiltY!,
|
|
884
|
+
overridePressure: brush.overridePosePressure!,
|
|
885
|
+
pressure: parsePercent(brush.brushPosePressure!),
|
|
886
|
+
tiltX: brush.brushPoseTiltX!,
|
|
887
|
+
tiltY: brush.brushPoseTiltY!,
|
|
888
|
+
angle: brush.brushPoseAngle!,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const to = brush.toolOptions;
|
|
893
|
+
if (to) {
|
|
894
|
+
b.toolOptions = {
|
|
895
|
+
type: toBrushType[to._classID] || 'brush',
|
|
896
|
+
brushPreset: to.brushPreset,
|
|
897
|
+
flow: to.flow ?? 100,
|
|
898
|
+
smooth: to.Smoo ?? 0,
|
|
899
|
+
mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
|
|
900
|
+
opacity: to.Opct ?? 100,
|
|
901
|
+
smoothing: !!to.smoothing,
|
|
902
|
+
smoothingValue: to.smoothingValue || 0,
|
|
903
|
+
smoothingRadiusMode: !!to.smoothingRadiusMode,
|
|
904
|
+
smoothingCatchup: !!to.smoothingCatchup,
|
|
905
|
+
smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
|
|
906
|
+
smoothingZoomCompensation: !!to.smoothingZoomCompensation,
|
|
907
|
+
pressureSmoothing: !!to.pressureSmoothing,
|
|
908
|
+
usePressureOverridesSize: !!to.usePressureOverridesSize,
|
|
909
|
+
usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
|
|
910
|
+
useLegacy: !!to.useLegacy,
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
if (to.prVr) b.toolOptions.flowDynamics = parseDynamics(to.prVr);
|
|
914
|
+
if (to.opVr) b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
|
|
915
|
+
if (to.szVr) b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
|
|
916
|
+
if ('wetness' in to) b.toolOptions.wetness = to.wetness;
|
|
917
|
+
if ('dryness' in to) b.toolOptions.dryness = to.dryness;
|
|
918
|
+
if ('mix' in to) b.toolOptions.mix = to.mix;
|
|
919
|
+
if ('autoFill' in to) b.toolOptions.autoFill = to.autoFill;
|
|
920
|
+
if ('autoClean' in to) b.toolOptions.autoClean = to.autoClean;
|
|
921
|
+
if ('loadSolidColorOnly' in to) b.toolOptions.loadSolidColorOnly = to.loadSolidColorOnly;
|
|
922
|
+
if ('sampleAllLayers' in to) b.toolOptions.sampleAllLayers = to.sampleAllLayers;
|
|
923
|
+
if ('SmdF' in to) b.toolOptions.smudgeFingerPainting = to.SmdF;
|
|
924
|
+
if ('SmdS' in to) b.toolOptions.smudgeSampleAllLayers = to.SmdS;
|
|
925
|
+
if ('Prs ' in to) b.toolOptions.strength = to['Prs '];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return b;
|
|
929
|
+
}
|