@yft-design/psd-lib 1.0.1

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/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @yft-design/psd-lib
2
+
3
+ A fast PSD (Photoshop) file parser for Node.js and browsers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @yft-design/psd-lib
9
+ # or
10
+ pnpm install @yft-design/psd-lib
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - Parse and write PSD files
16
+ - Support for layers, effects, text, and more
17
+ - Read ABR (brush) and CSH (custom shape) files
18
+ - TypeScript support
19
+ - Works in Node.js and browsers
20
+
21
+ ## Usage
22
+
23
+ ```typescript
24
+ import { readPsd, writePsd } from '@yft-design/psd-lib'
25
+
26
+ // Read a PSD file
27
+ const psd = readPsd(buffer)
28
+
29
+ // Access layers
30
+ console.log(psd.children) // layer tree
31
+
32
+ // Write a PSD file
33
+ const output = writePsd(psd)
34
+ ```
35
+
36
+ ## API
37
+
38
+ - `readPsd(buffer, options?)` - Parse a PSD file from ArrayBuffer
39
+ - `writePsd(psd, options?)` - Write a PSD to ArrayBuffer
40
+ - `writePsdUint8Array(psd, options?)` - Write a PSD to Uint8Array
41
+ - `writePsdBuffer(psd, options?)` - Write a PSD to Node.js Buffer
42
+
43
+ ## License
44
+
45
+ MIT
package/dist/abr.d.ts ADDED
@@ -0,0 +1,143 @@
1
+ import { BlendMode, PatternInfo } from './psd';
2
+ export interface Abr {
3
+ brushes: Brush[];
4
+ samples: SampleInfo[];
5
+ patterns: PatternInfo[];
6
+ }
7
+ export interface SampleInfo {
8
+ id: string;
9
+ bounds: {
10
+ x: number;
11
+ y: number;
12
+ w: number;
13
+ h: number;
14
+ };
15
+ alpha: Uint8Array;
16
+ }
17
+ export interface BrushDynamics {
18
+ control: 'off' | 'fade' | 'pen pressure' | 'pen tilt' | 'stylus wheel' | 'initial direction' | 'direction' | 'initial rotation' | 'rotation';
19
+ steps: number;
20
+ jitter: number;
21
+ minimum: number;
22
+ }
23
+ export interface BrushShape {
24
+ name?: string;
25
+ size: number;
26
+ angle: number;
27
+ roundness: number;
28
+ hardness?: number;
29
+ spacingOn: boolean;
30
+ spacing: number;
31
+ flipX: boolean;
32
+ flipY: boolean;
33
+ sampledData?: string;
34
+ }
35
+ export interface Brush {
36
+ name: string;
37
+ shape: BrushShape;
38
+ shapeDynamics?: {
39
+ sizeDynamics: BrushDynamics;
40
+ minimumDiameter: number;
41
+ tiltScale: number;
42
+ angleDynamics: BrushDynamics;
43
+ roundnessDynamics: BrushDynamics;
44
+ minimumRoundness: number;
45
+ flipX: boolean;
46
+ flipY: boolean;
47
+ brushProjection: boolean;
48
+ };
49
+ scatter?: {
50
+ bothAxes: boolean;
51
+ scatterDynamics: BrushDynamics;
52
+ countDynamics: BrushDynamics;
53
+ count: number;
54
+ };
55
+ texture?: {
56
+ id: string;
57
+ name: string;
58
+ invert: boolean;
59
+ scale: number;
60
+ brightness: number;
61
+ contrast: number;
62
+ blendMode: BlendMode;
63
+ depth: number;
64
+ depthMinimum: number;
65
+ depthDynamics: BrushDynamics;
66
+ };
67
+ dualBrush?: {
68
+ flip: boolean;
69
+ shape: BrushShape;
70
+ blendMode: BlendMode;
71
+ useScatter: boolean;
72
+ spacing: number;
73
+ count: number;
74
+ bothAxes: boolean;
75
+ countDynamics: BrushDynamics;
76
+ scatterDynamics: BrushDynamics;
77
+ };
78
+ colorDynamics?: {
79
+ foregroundBackground: BrushDynamics;
80
+ hue: number;
81
+ saturation: number;
82
+ brightness: number;
83
+ purity: number;
84
+ perTip: boolean;
85
+ };
86
+ transfer?: {
87
+ flowDynamics: BrushDynamics;
88
+ opacityDynamics: BrushDynamics;
89
+ wetnessDynamics: BrushDynamics;
90
+ mixDynamics: BrushDynamics;
91
+ };
92
+ brushPose?: {
93
+ overrideAngle: boolean;
94
+ overrideTiltX: boolean;
95
+ overrideTiltY: boolean;
96
+ overridePressure: boolean;
97
+ pressure: number;
98
+ tiltX: number;
99
+ tiltY: number;
100
+ angle: number;
101
+ };
102
+ noise: boolean;
103
+ wetEdges: boolean;
104
+ protectTexture?: boolean;
105
+ spacing: number;
106
+ brushGroup?: undefined;
107
+ interpretation?: boolean;
108
+ useBrushSize: boolean;
109
+ toolOptions?: {
110
+ type: 'brush' | 'mixer brush' | 'smudge brush';
111
+ brushPreset: boolean;
112
+ flow: number;
113
+ wetness?: number;
114
+ dryness?: number;
115
+ mix?: number;
116
+ smooth: number;
117
+ mode: BlendMode;
118
+ opacity: number;
119
+ smoothing: boolean;
120
+ smoothingValue: number;
121
+ smoothingRadiusMode: boolean;
122
+ smoothingCatchup: boolean;
123
+ smoothingCatchupAtEnd: boolean;
124
+ smoothingZoomCompensation: boolean;
125
+ pressureSmoothing: boolean;
126
+ usePressureOverridesSize: boolean;
127
+ usePressureOverridesOpacity: boolean;
128
+ useLegacy: boolean;
129
+ autoFill?: boolean;
130
+ autoClean?: boolean;
131
+ loadSolidColorOnly?: boolean;
132
+ sampleAllLayers?: boolean;
133
+ flowDynamics?: BrushDynamics;
134
+ opacityDynamics?: BrushDynamics;
135
+ sizeDynamics?: BrushDynamics;
136
+ smudgeFingerPainting?: boolean;
137
+ smudgeSampleAllLayers?: boolean;
138
+ strength?: number;
139
+ };
140
+ }
141
+ export declare function readAbr(buffer: ArrayBufferView, options?: {
142
+ logMissingFeatures?: boolean;
143
+ }): Abr;
package/dist/abr.js ADDED
@@ -0,0 +1,300 @@
1
+ import { BlnM, parseAngle, parsePercent, parseUnitsToNumber, readVersionAndDescriptor } from './descriptor';
2
+ import { checkSignature, createReader, readBytes, readDataRLE, readInt16, readInt32, readPascalString, readPattern, readSignature, readUint16, readUint32, readUint8, skipBytes } from './psdReader';
3
+ const dynamicsControl = ['off', 'fade', 'pen pressure', 'pen tilt', 'stylus wheel', 'initial direction', 'direction', 'initial rotation', 'rotation'];
4
+ const toBrushType = {
5
+ _: 'brush',
6
+ MixB: 'mixer brush',
7
+ SmTl: 'smudge brush',
8
+ // PbTl
9
+ // ErTl
10
+ };
11
+ function parseDynamics(desc) {
12
+ return {
13
+ control: dynamicsControl[desc.bVTy],
14
+ steps: desc.fStp,
15
+ jitter: parsePercent(desc.jitter),
16
+ minimum: parsePercent(desc['Mnm ']),
17
+ };
18
+ }
19
+ function parseBrushShape(desc) {
20
+ const shape = {
21
+ size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
22
+ angle: parseAngle(desc.Angl),
23
+ roundness: parsePercent(desc.Rndn),
24
+ spacingOn: desc.Intr,
25
+ spacing: parsePercent(desc.Spcn),
26
+ flipX: desc.flipX,
27
+ flipY: desc.flipY,
28
+ };
29
+ if (desc['Nm '])
30
+ shape.name = desc['Nm '];
31
+ if (desc.Hrdn)
32
+ shape.hardness = parsePercent(desc.Hrdn);
33
+ if (desc.sampledData)
34
+ shape.sampledData = desc.sampledData;
35
+ return shape;
36
+ }
37
+ export function readAbr(buffer, options = {}) {
38
+ const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength);
39
+ const version = readInt16(reader);
40
+ const samples = [];
41
+ const brushes = [];
42
+ const patterns = [];
43
+ if (version === 1 || version === 2) {
44
+ throw new Error(`Unsupported ABR version (${version})`); // TODO: ...
45
+ }
46
+ else if (version === 6 || version === 7 || version === 9 || version === 10) {
47
+ const minorVersion = readInt16(reader);
48
+ if (minorVersion !== 1 && minorVersion !== 2)
49
+ throw new Error('Unsupported ABR minor version');
50
+ while (reader.offset < reader.view.byteLength) {
51
+ checkSignature(reader, '8BIM');
52
+ const type = readSignature(reader);
53
+ let size = readUint32(reader);
54
+ const end = reader.offset + size;
55
+ switch (type) {
56
+ case 'samp': {
57
+ while (reader.offset < end) {
58
+ let brushLength = readUint32(reader);
59
+ while (brushLength & 0b11)
60
+ brushLength++; // pad to 4 byte alignment
61
+ const brushEnd = reader.offset + brushLength;
62
+ const id = readPascalString(reader, 1);
63
+ // v1 - Skip the Int16 bounds rectangle and the unknown Int16.
64
+ // v2 - Skip the unknown bytes.
65
+ skipBytes(reader, minorVersion === 1 ? 10 : 264);
66
+ const y = readInt32(reader);
67
+ const x = readInt32(reader);
68
+ const h = readInt32(reader) - y;
69
+ const w = readInt32(reader) - x;
70
+ if (w <= 0 || h <= 0)
71
+ throw new Error('Invalid bounds');
72
+ const bithDepth = readInt16(reader);
73
+ const compression = readUint8(reader); // 0 - raw, 1 - RLE
74
+ const alpha = new Uint8Array(w * h);
75
+ if (bithDepth === 8) {
76
+ if (compression === 0) {
77
+ alpha.set(readBytes(reader, alpha.byteLength));
78
+ }
79
+ else if (compression === 1) {
80
+ readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, bithDepth, 1, [0], false);
81
+ }
82
+ else {
83
+ throw new Error('Invalid compression');
84
+ }
85
+ }
86
+ else if (bithDepth === 16) {
87
+ if (compression === 0) {
88
+ for (let i = 0; i < alpha.byteLength; i++) {
89
+ alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
90
+ }
91
+ }
92
+ else if (compression === 1) {
93
+ throw new Error('not implemented (16bit RLE)'); // TODO: ...
94
+ }
95
+ else {
96
+ throw new Error('Invalid compression');
97
+ }
98
+ }
99
+ else {
100
+ throw new Error('Invalid depth');
101
+ }
102
+ samples.push({ id, bounds: { x, y, w, h }, alpha });
103
+ reader.offset = brushEnd;
104
+ }
105
+ break;
106
+ }
107
+ case 'desc': {
108
+ const desc = readVersionAndDescriptor(reader, true);
109
+ // console.log(require('util').inspect(desc, false, 99, true));
110
+ for (const brush of desc.Brsh) {
111
+ const b = {
112
+ name: brush['Nm '],
113
+ shape: parseBrushShape(brush.Brsh),
114
+ spacing: parsePercent(brush.Spcn),
115
+ // TODO: brushGroup ???
116
+ wetEdges: brush.Wtdg,
117
+ noise: brush.Nose,
118
+ // TODO: TxtC ??? smoothing / build-up ?
119
+ // TODO: 'Rpt ' ???
120
+ useBrushSize: brush.useBrushSize, // ???
121
+ };
122
+ if (brush.interpretation != null)
123
+ b.interpretation = brush.interpretation;
124
+ if (brush.protectTexture != null)
125
+ b.protectTexture = brush.protectTexture;
126
+ if (brush.useTipDynamics) {
127
+ b.shapeDynamics = {
128
+ tiltScale: parsePercent(brush.tiltScale),
129
+ sizeDynamics: parseDynamics(brush.szVr),
130
+ angleDynamics: parseDynamics(brush.angleDynamics),
131
+ roundnessDynamics: parseDynamics(brush.roundnessDynamics),
132
+ flipX: brush.flipX,
133
+ flipY: brush.flipY,
134
+ brushProjection: brush.brushProjection,
135
+ minimumDiameter: parsePercent(brush.minimumDiameter),
136
+ minimumRoundness: parsePercent(brush.minimumRoundness),
137
+ };
138
+ }
139
+ if (brush.useScatter) {
140
+ b.scatter = {
141
+ count: brush['Cnt '],
142
+ bothAxes: brush.bothAxes,
143
+ countDynamics: parseDynamics(brush.countDynamics),
144
+ scatterDynamics: parseDynamics(brush.scatterDynamics),
145
+ };
146
+ }
147
+ if (brush.useTexture && brush.Txtr) {
148
+ b.texture = {
149
+ id: brush.Txtr.Idnt,
150
+ name: brush.Txtr['Nm '],
151
+ blendMode: BlnM.decode(brush.textureBlendMode),
152
+ depth: parsePercent(brush.textureDepth),
153
+ depthMinimum: parsePercent(brush.minimumDepth),
154
+ depthDynamics: parseDynamics(brush.textureDepthDynamics),
155
+ scale: parsePercent(brush.textureScale),
156
+ invert: brush.InvT,
157
+ brightness: brush.textureBrightness,
158
+ contrast: brush.textureContrast,
159
+ };
160
+ }
161
+ const db = brush.dualBrush;
162
+ if (db && db.useDualBrush) {
163
+ b.dualBrush = {
164
+ flip: db.Flip,
165
+ shape: parseBrushShape(db.Brsh),
166
+ blendMode: BlnM.decode(db.BlnM),
167
+ useScatter: db.useScatter,
168
+ spacing: parsePercent(db.Spcn),
169
+ count: db['Cnt '],
170
+ bothAxes: db.bothAxes,
171
+ countDynamics: parseDynamics(db.countDynamics),
172
+ scatterDynamics: parseDynamics(db.scatterDynamics),
173
+ };
174
+ }
175
+ if (brush.useColorDynamics) {
176
+ b.colorDynamics = {
177
+ foregroundBackground: parseDynamics(brush.clVr),
178
+ hue: parsePercent(brush['H ']),
179
+ saturation: parsePercent(brush.Strt),
180
+ brightness: parsePercent(brush.Brgh),
181
+ purity: parsePercent(brush.purity),
182
+ perTip: brush.colorDynamicsPerTip,
183
+ };
184
+ }
185
+ if (brush.usePaintDynamics) {
186
+ b.transfer = {
187
+ flowDynamics: parseDynamics(brush.prVr),
188
+ opacityDynamics: parseDynamics(brush.opVr),
189
+ wetnessDynamics: parseDynamics(brush.wtVr),
190
+ mixDynamics: parseDynamics(brush.mxVr),
191
+ };
192
+ }
193
+ if (brush.useBrushPose) {
194
+ b.brushPose = {
195
+ overrideAngle: brush.overridePoseAngle,
196
+ overrideTiltX: brush.overridePoseTiltX,
197
+ overrideTiltY: brush.overridePoseTiltY,
198
+ overridePressure: brush.overridePosePressure,
199
+ pressure: parsePercent(brush.brushPosePressure),
200
+ tiltX: brush.brushPoseTiltX,
201
+ tiltY: brush.brushPoseTiltY,
202
+ angle: brush.brushPoseAngle,
203
+ };
204
+ }
205
+ const to = brush.toolOptions;
206
+ if (to) {
207
+ b.toolOptions = {
208
+ type: toBrushType[to._classID] || 'brush',
209
+ brushPreset: to.brushPreset,
210
+ flow: to.flow ?? 100,
211
+ smooth: to.Smoo ?? 0,
212
+ mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
213
+ opacity: to.Opct ?? 100,
214
+ smoothing: !!to.smoothing,
215
+ smoothingValue: to.smoothingValue || 0,
216
+ smoothingRadiusMode: !!to.smoothingRadiusMode,
217
+ smoothingCatchup: !!to.smoothingCatchup,
218
+ smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
219
+ smoothingZoomCompensation: !!to.smoothingZoomCompensation,
220
+ pressureSmoothing: !!to.pressureSmoothing,
221
+ usePressureOverridesSize: !!to.usePressureOverridesSize,
222
+ usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
223
+ useLegacy: !!to.useLegacy,
224
+ };
225
+ if (to.prVr)
226
+ b.toolOptions.flowDynamics = parseDynamics(to.prVr);
227
+ if (to.opVr)
228
+ b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
229
+ if (to.szVr)
230
+ b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
231
+ if ('wetness' in to)
232
+ b.toolOptions.wetness = to.wetness;
233
+ if ('dryness' in to)
234
+ b.toolOptions.dryness = to.dryness;
235
+ if ('mix' in to)
236
+ b.toolOptions.mix = to.mix;
237
+ if ('autoFill' in to)
238
+ b.toolOptions.autoFill = to.autoFill;
239
+ if ('autoClean' in to)
240
+ b.toolOptions.autoClean = to.autoClean;
241
+ if ('loadSolidColorOnly' in to)
242
+ b.toolOptions.loadSolidColorOnly = to.loadSolidColorOnly;
243
+ if ('sampleAllLayers' in to)
244
+ b.toolOptions.sampleAllLayers = to.sampleAllLayers;
245
+ if ('SmdF' in to)
246
+ b.toolOptions.smudgeFingerPainting = to.SmdF;
247
+ if ('SmdS' in to)
248
+ b.toolOptions.smudgeSampleAllLayers = to.SmdS;
249
+ if ('Prs ' in to)
250
+ b.toolOptions.strength = to['Prs '];
251
+ if ('SmdF' in to)
252
+ b.toolOptions.smudgeFingerPainting = to.SmdF;
253
+ if ('SmdS' in to)
254
+ b.toolOptions.smudgeSampleAllLayers = to.SmdS;
255
+ }
256
+ brushes.push(b);
257
+ }
258
+ break;
259
+ }
260
+ case 'patt': {
261
+ if (reader.offset < end) { // TODO: check multiple patterns
262
+ patterns.push(readPattern(reader));
263
+ reader.offset = end;
264
+ }
265
+ break;
266
+ }
267
+ case 'phry': {
268
+ // TODO: what is this ?
269
+ const desc = readVersionAndDescriptor(reader);
270
+ // example:
271
+ // hierarchy: [
272
+ // {
273
+ // 'Nm ': 'PRE_EXPORT ',
274
+ // zuid: '965209f2-6f35-9a40-aa43-485684382172'
275
+ // },
276
+ // {},
277
+ // ...
278
+ // ]
279
+ if (options.logMissingFeatures) {
280
+ if (desc.hierarchy?.length) {
281
+ // console.log('unhandled phry section', desc);
282
+ }
283
+ }
284
+ break;
285
+ }
286
+ default:
287
+ throw new Error(`Invalid brush type: ${type}`);
288
+ }
289
+ // align to 4 bytes
290
+ while (size % 4) {
291
+ reader.offset++;
292
+ size++;
293
+ }
294
+ }
295
+ }
296
+ else {
297
+ throw new Error(`Unsupported ABR version (${version})`);
298
+ }
299
+ return { samples, patterns, brushes };
300
+ }
@@ -0,0 +1,26 @@
1
+ import { LayerAdditionalInfo, BezierPath, Psd, WriteOptions, BooleanOperation, LayerEffectsInfo, LayerVectorMask } from './psd';
2
+ import { PsdReader } from './psdReader';
3
+ import { PsdWriter } from './psdWriter';
4
+ import type { InternalImageResources } from './imageResources';
5
+ export interface ExtendedWriteOptions extends WriteOptions {
6
+ layerIds: Set<number>;
7
+ layerToId: Map<any, number>;
8
+ }
9
+ type HasMethod = (target: LayerAdditionalInfo) => boolean;
10
+ type ReadMethod = (reader: PsdReader, target: LayerAdditionalInfo, left: () => number, psd: Psd, imageResources: InternalImageResources) => void;
11
+ type WriteMethod = (writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) => void;
12
+ export interface InfoHandler {
13
+ key: string;
14
+ has: HasMethod;
15
+ read: ReadMethod;
16
+ write: WriteMethod;
17
+ }
18
+ export declare const infoHandlers: InfoHandler[];
19
+ export declare const infoHandlersMap: {
20
+ [key: string]: InfoHandler;
21
+ };
22
+ export declare function readBezierKnot(reader: PsdReader, width: number, height: number): number[];
23
+ export declare const booleanOperations: BooleanOperation[];
24
+ export declare function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, width: number, height: number, size: number): BezierPath[];
25
+ export declare function hasMultiEffects(effects: LayerEffectsInfo): boolean;
26
+ export {};