ag-psd 15.0.4 → 15.1.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/src/abr.ts ADDED
@@ -0,0 +1,540 @@
1
+ import { BlnM, DescriptorUnitsValue, parseAngle, parsePercent, parseUnitsToNumber, readVersionAndDescriptor } from './descriptor';
2
+ import { BlendMode, PatternInfo } from './psd';
3
+ import {
4
+ checkSignature, createReader, readBytes, readDataRLE, readInt16, readInt32, readPascalString, readPattern,
5
+ readSignature, readUint16, readUint32, readUint8, skipBytes
6
+ } from './psdReader';
7
+
8
+ export interface Abr {
9
+ brushes: Brush[];
10
+ samples: SampleInfo[];
11
+ patterns: PatternInfo[];
12
+ }
13
+
14
+ export interface SampleInfo {
15
+ id: string;
16
+ bounds: { x: number; y: number; w: number; h: number; };
17
+ alpha: Uint8Array;
18
+ }
19
+
20
+ export interface BrushDynamics {
21
+ control: 'off' | 'fade' | 'pen pressure' | 'pen tilt' | 'stylus wheel' | 'initial direction' | 'direction' | 'initial rotation' | 'rotation';
22
+ steps: number; // for fade
23
+ jitter: number;
24
+ minimum: number;
25
+ }
26
+
27
+ const dynamicsControl = ['off', 'fade', 'pen pressure', 'pen tilt', 'stylus wheel', 'initial direction', 'direction', 'initial rotation', 'rotation'];
28
+
29
+ export interface BrushShape {
30
+ name?: string;
31
+ size: number;
32
+ angle: number;
33
+ roundness: number;
34
+ hardness?: number;
35
+ spacingOn: boolean;
36
+ spacing: number;
37
+ flipX: boolean;
38
+ flipY: boolean;
39
+ sampledData?: string;
40
+ }
41
+
42
+ export interface Brush {
43
+ name: string;
44
+ shape: BrushShape;
45
+ shapeDynamics?: {
46
+ sizeDynamics: BrushDynamics;
47
+ minimumDiameter: number;
48
+ tiltScale: number;
49
+ angleDynamics: BrushDynamics; // jitter 0-1 -> 0-360 deg ?
50
+ roundnessDynamics: BrushDynamics;
51
+ minimumRoundness: number;
52
+ flipX: boolean;
53
+ flipY: boolean;
54
+ brushProjection: boolean;
55
+ };
56
+ scatter?: {
57
+ bothAxes: boolean;
58
+ scatterDynamics: BrushDynamics;
59
+ countDynamics: BrushDynamics;
60
+ count: number;
61
+ };
62
+ texture?: {
63
+ id: string;
64
+ name: string;
65
+ invert: boolean;
66
+ scale: number;
67
+ brightness: number;
68
+ contrast: number;
69
+ blendMode: BlendMode;
70
+ depth: number;
71
+ depthMinimum: number;
72
+ depthDynamics: BrushDynamics;
73
+ };
74
+ dualBrush?: {
75
+ flip: boolean;
76
+ shape: BrushShape;
77
+ blendMode: BlendMode;
78
+ useScatter: boolean;
79
+ spacing: number;
80
+ count: number;
81
+ bothAxes: boolean;
82
+ countDynamics: BrushDynamics;
83
+ scatterDynamics: BrushDynamics;
84
+ };
85
+ colorDynamics?: {
86
+ foregroundBackground: BrushDynamics;
87
+ hue: number;
88
+ saturation: number;
89
+ brightness: number;
90
+ purity: number;
91
+ perTip: boolean;
92
+ };
93
+ transfer?: {
94
+ flowDynamics: BrushDynamics;
95
+ opacityDynamics: BrushDynamics;
96
+ wetnessDynamics: BrushDynamics;
97
+ mixDynamics: BrushDynamics;
98
+ };
99
+ brushPose?: {
100
+ overrideAngle: boolean;
101
+ overrideTiltX: boolean;
102
+ overrideTiltY: boolean;
103
+ overridePressure: boolean;
104
+ pressure: number;
105
+ tiltX: number;
106
+ tiltY: number;
107
+ angle: number;
108
+ };
109
+ noise: boolean;
110
+ wetEdges: boolean;
111
+ // TODO: build-up
112
+ // TODO: smoothing
113
+ protectTexture?: boolean;
114
+ spacing: number;
115
+ brushGroup?: undefined; // ?
116
+ interpretation?: boolean; // ?
117
+ useBrushSize: boolean; // ?
118
+ toolOptions?: {
119
+ brushPreset: boolean;
120
+ flow: number; // 0-100
121
+ smooth: number; // ?
122
+ mode: BlendMode;
123
+ opacity: number; // 0-100
124
+ smoothing: boolean;
125
+ smoothingValue: number;
126
+ smoothingRadiusMode: boolean;
127
+ smoothingCatchup: boolean;
128
+ smoothingCatchupAtEnd: boolean;
129
+ smoothingZoomCompensation: boolean;
130
+ pressureSmoothing: boolean;
131
+ usePressureOverridesSize: boolean;
132
+ usePressureOverridesOpacity: boolean;
133
+ useLegacy: boolean;
134
+ flowDynamics?: BrushDynamics;
135
+ opacityDynamics?: BrushDynamics;
136
+ sizeDynamics?: BrushDynamics;
137
+ };
138
+ }
139
+
140
+ // internal
141
+
142
+ interface PhryDescriptor {
143
+ hierarchy: ({} | {
144
+ 'Nm ': string;
145
+ zuid: string;
146
+ })[];
147
+ }
148
+
149
+ interface DynamicsDescriptor {
150
+ bVTy: number;
151
+ fStp: number;
152
+ jitter: DescriptorUnitsValue;
153
+ 'Mnm ': DescriptorUnitsValue;
154
+ }
155
+
156
+ interface BrushShapeDescriptor {
157
+ Dmtr: DescriptorUnitsValue;
158
+ Angl: DescriptorUnitsValue;
159
+ Rndn: DescriptorUnitsValue;
160
+ 'Nm '?: string;
161
+ Spcn: DescriptorUnitsValue;
162
+ Intr: boolean;
163
+ Hrdn?: DescriptorUnitsValue;
164
+ flipX: boolean;
165
+ flipY: boolean;
166
+ sampledData?: string;
167
+ }
168
+
169
+ interface DescDescriptor {
170
+ Brsh: {
171
+ 'Nm ': string;
172
+ Brsh: BrushShapeDescriptor;
173
+ useTipDynamics: boolean;
174
+ flipX: boolean;
175
+ flipY: boolean;
176
+ brushProjection: boolean;
177
+ minimumDiameter: DescriptorUnitsValue;
178
+ minimumRoundness: DescriptorUnitsValue;
179
+ tiltScale: DescriptorUnitsValue;
180
+ szVr: DynamicsDescriptor;
181
+ angleDynamics: DynamicsDescriptor;
182
+ roundnessDynamics: DynamicsDescriptor;
183
+ useScatter: boolean;
184
+ Spcn: DescriptorUnitsValue;
185
+ 'Cnt ': number;
186
+ bothAxes: boolean;
187
+ countDynamics: DynamicsDescriptor;
188
+ scatterDynamics: DynamicsDescriptor;
189
+ dualBrush: { useDualBrush: false; } | {
190
+ useDualBrush: true;
191
+ Flip: boolean;
192
+ Brsh: BrushShapeDescriptor;
193
+ BlnM: string;
194
+ useScatter: boolean;
195
+ Spcn: DescriptorUnitsValue;
196
+ 'Cnt ': number;
197
+ bothAxes: boolean;
198
+ countDynamics: DynamicsDescriptor;
199
+ scatterDynamics: DynamicsDescriptor;
200
+ };
201
+ brushGroup: { useBrushGroup: false; };
202
+ useTexture: boolean;
203
+ TxtC: boolean;
204
+ interpretation: boolean;
205
+ textureBlendMode: string;
206
+ textureDepth: DescriptorUnitsValue;
207
+ minimumDepth: DescriptorUnitsValue;
208
+ textureDepthDynamics: DynamicsDescriptor;
209
+ Txtr?: {
210
+ 'Nm ': string;
211
+ Idnt: string;
212
+ };
213
+ textureScale: DescriptorUnitsValue;
214
+ InvT: boolean;
215
+ protectTexture: boolean;
216
+ textureBrightness: number;
217
+ textureContrast: number;
218
+ usePaintDynamics: boolean;
219
+ prVr?: DynamicsDescriptor;
220
+ opVr?: DynamicsDescriptor;
221
+ wtVr?: DynamicsDescriptor;
222
+ mxVr?: DynamicsDescriptor;
223
+ useColorDynamics: boolean;
224
+ clVr?: DynamicsDescriptor;
225
+ 'H '?: DescriptorUnitsValue;
226
+ Strt?: DescriptorUnitsValue;
227
+ Brgh?: DescriptorUnitsValue;
228
+ purity?: DescriptorUnitsValue;
229
+ colorDynamicsPerTip?: true;
230
+ Wtdg: boolean;
231
+ Nose: boolean;
232
+ 'Rpt ': boolean;
233
+ useBrushSize: boolean;
234
+ useBrushPose: boolean;
235
+ overridePoseAngle?: boolean;
236
+ overridePoseTiltX?: boolean;
237
+ overridePoseTiltY?: boolean;
238
+ overridePosePressure?: boolean;
239
+ brushPosePressure?: DescriptorUnitsValue;
240
+ brushPoseTiltX?: number;
241
+ brushPoseTiltY?: number;
242
+ brushPoseAngle?: number;
243
+ toolOptions?: {
244
+ brushPreset: boolean;
245
+ flow?: number;
246
+ Smoo?: number;
247
+ 'Md ': string;
248
+ Opct?: number;
249
+ smoothing?: boolean;
250
+ smoothingValue?: number;
251
+ smoothingRadiusMode?: boolean;
252
+ smoothingCatchup?: boolean;
253
+ smoothingCatchupAtEnd?: boolean;
254
+ smoothingZoomCompensation?: boolean;
255
+ pressureSmoothing?: boolean;
256
+ usePressureOverridesSize?: boolean;
257
+ usePressureOverridesOpacity?: boolean;
258
+ useLegacy: boolean;
259
+ 'Prs '?: number; // TODO: ???
260
+ MgcE?: boolean; // TODO: ???
261
+ ErsB?: number; // TODO: ???
262
+ prVr?: DynamicsDescriptor;
263
+ opVr?: DynamicsDescriptor;
264
+ szVr?: DynamicsDescriptor;
265
+ };
266
+ }[];
267
+ }
268
+
269
+ function parseDynamics(desc: DynamicsDescriptor): BrushDynamics {
270
+ return {
271
+ control: dynamicsControl[desc.bVTy] as any,
272
+ steps: desc.fStp,
273
+ jitter: parsePercent(desc.jitter),
274
+ minimum: parsePercent(desc['Mnm ']),
275
+ };
276
+ }
277
+
278
+ function parseBrushShape(desc: BrushShapeDescriptor): BrushShape {
279
+ const shape: BrushShape = {
280
+ size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
281
+ angle: parseAngle(desc.Angl),
282
+ roundness: parsePercent(desc.Rndn),
283
+ spacingOn: desc.Intr,
284
+ spacing: parsePercent(desc.Spcn),
285
+ flipX: desc.flipX,
286
+ flipY: desc.flipY,
287
+ };
288
+
289
+ if (desc['Nm ']) shape.name = desc['Nm '];
290
+ if (desc.Hrdn) shape.hardness = parsePercent(desc.Hrdn);
291
+ if (desc.sampledData) shape.sampledData = desc.sampledData;
292
+
293
+ return shape;
294
+ }
295
+
296
+ export function readAbr(buffer: ArrayBufferView, options: { logMissingFeatures?: boolean; } = {}): Abr {
297
+ const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength);
298
+ const version = readInt16(reader);
299
+ const samples: SampleInfo[] = [];
300
+ const brushes: Brush[] = [];
301
+ const patterns: PatternInfo[] = [];
302
+
303
+ if (version === 1 || version === 2) {
304
+ throw new Error(`Unsupported ABR version (${version})`); // TODO: ...
305
+ } else if (version === 6 || version === 7 || version === 9 || version === 10) {
306
+ const minorVersion = readInt16(reader);
307
+ if (minorVersion !== 1 && minorVersion !== 2) throw new Error('Unsupported ABR minor version');
308
+
309
+ while (reader.offset < reader.view.byteLength) {
310
+ checkSignature(reader, '8BIM');
311
+ const type = readSignature(reader) as 'samp' | 'desc' | 'patt' | 'phry';
312
+ let size = readUint32(reader);
313
+ const end = reader.offset + size;
314
+
315
+ switch (type) {
316
+ case 'samp': {
317
+ while (reader.offset < end) {
318
+ let brushLength = readUint32(reader);
319
+ while (brushLength & 0b11) brushLength++; // pad to 4 byte alignment
320
+ const brushEnd = reader.offset + brushLength;
321
+
322
+ const id = readPascalString(reader, 1);
323
+
324
+ // v1 - Skip the Int16 bounds rectangle and the unknown Int16.
325
+ // v2 - Skip the unknown bytes.
326
+ skipBytes(reader, minorVersion === 1 ? 10 : 264);
327
+
328
+ const y = readInt32(reader);
329
+ const x = readInt32(reader);
330
+ const h = readInt32(reader) - y;
331
+ const w = readInt32(reader) - x;
332
+ if (w <= 0 || h <= 0) throw new Error('Invalid bounds');
333
+
334
+ const depth = readInt16(reader);
335
+ const compression = readUint8(reader); // 0 - raw, 1 - RLE
336
+ const alpha = new Uint8Array(w * h);
337
+
338
+ if (depth === 8) {
339
+ if (compression === 0) {
340
+ alpha.set(readBytes(reader, alpha.byteLength));
341
+ } else if (compression === 1) {
342
+ readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, 1, [0], false);
343
+ } else {
344
+ throw new Error('Invalid compression');
345
+ }
346
+ } else if (depth === 16) {
347
+ if (compression === 0) {
348
+ for (let i = 0; i < alpha.byteLength; i++) {
349
+ alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
350
+ }
351
+ } else if (compression === 1) {
352
+ throw new Error('not implemented (16bit RLE)'); // TODO: ...
353
+ } else {
354
+ throw new Error('Invalid compression');
355
+ }
356
+ } else {
357
+ throw new Error('Invalid depth');
358
+ }
359
+
360
+ samples.push({ id, bounds: { x, y, w, h }, alpha });
361
+ reader.offset = brushEnd;
362
+ }
363
+ break;
364
+ }
365
+ case 'desc': {
366
+ const desc: DescDescriptor = readVersionAndDescriptor(reader);
367
+ // console.log(require('util').inspect(desc, false, 99, true));
368
+
369
+ for (const brush of desc.Brsh) {
370
+ const b: Brush = {
371
+ name: brush['Nm '],
372
+ shape: parseBrushShape(brush.Brsh),
373
+ spacing: parsePercent(brush.Spcn),
374
+ // TODO: brushGroup ???
375
+ wetEdges: brush.Wtdg,
376
+ noise: brush.Nose,
377
+ // TODO: TxtC ??? smoothing / build-up ?
378
+ // TODO: 'Rpt ' ???
379
+ useBrushSize: brush.useBrushSize, // ???
380
+ };
381
+
382
+ if (brush.interpretation != null) b.interpretation = brush.interpretation;
383
+ if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;
384
+
385
+ if (brush.useTipDynamics) {
386
+ b.shapeDynamics = {
387
+ tiltScale: parsePercent(brush.tiltScale),
388
+ sizeDynamics: parseDynamics(brush.szVr),
389
+ angleDynamics: parseDynamics(brush.angleDynamics),
390
+ roundnessDynamics: parseDynamics(brush.roundnessDynamics),
391
+ flipX: brush.flipX,
392
+ flipY: brush.flipY,
393
+ brushProjection: brush.brushProjection,
394
+ minimumDiameter: parsePercent(brush.minimumDiameter),
395
+ minimumRoundness: parsePercent(brush.minimumRoundness),
396
+ };
397
+ }
398
+
399
+ if (brush.useScatter) {
400
+ b.scatter = {
401
+ count: brush['Cnt '],
402
+ bothAxes: brush.bothAxes,
403
+ countDynamics: parseDynamics(brush.countDynamics),
404
+ scatterDynamics: parseDynamics(brush.scatterDynamics),
405
+ };
406
+ }
407
+
408
+ if (brush.useTexture && brush.Txtr) {
409
+ b.texture = {
410
+ id: brush.Txtr.Idnt,
411
+ name: brush.Txtr['Nm '],
412
+ blendMode: BlnM.decode(brush.textureBlendMode),
413
+ depth: parsePercent(brush.textureDepth),
414
+ depthMinimum: parsePercent(brush.minimumDepth),
415
+ depthDynamics: parseDynamics(brush.textureDepthDynamics),
416
+ scale: parsePercent(brush.textureScale),
417
+ invert: brush.InvT,
418
+ brightness: brush.textureBrightness,
419
+ contrast: brush.textureContrast,
420
+ };
421
+ }
422
+
423
+ const db = brush.dualBrush;
424
+ if (db && db.useDualBrush) {
425
+ b.dualBrush = {
426
+ flip: db.Flip,
427
+ shape: parseBrushShape(db.Brsh),
428
+ blendMode: BlnM.decode(db.BlnM),
429
+ useScatter: db.useScatter,
430
+ spacing: parsePercent(db.Spcn),
431
+ count: db['Cnt '],
432
+ bothAxes: db.bothAxes,
433
+ countDynamics: parseDynamics(db.countDynamics),
434
+ scatterDynamics: parseDynamics(db.scatterDynamics),
435
+ };
436
+ }
437
+
438
+ if (brush.useColorDynamics) {
439
+ b.colorDynamics = {
440
+ foregroundBackground: parseDynamics(brush.clVr!),
441
+ hue: parsePercent(brush['H ']!),
442
+ saturation: parsePercent(brush.Strt!),
443
+ brightness: parsePercent(brush.Brgh!),
444
+ purity: parsePercent(brush.purity!),
445
+ perTip: brush.colorDynamicsPerTip!,
446
+ };
447
+ }
448
+
449
+ if (brush.usePaintDynamics) {
450
+ b.transfer = {
451
+ flowDynamics: parseDynamics(brush.prVr!),
452
+ opacityDynamics: parseDynamics(brush.opVr!),
453
+ wetnessDynamics: parseDynamics(brush.wtVr!),
454
+ mixDynamics: parseDynamics(brush.mxVr!),
455
+ };
456
+ }
457
+
458
+ if (brush.useBrushPose) {
459
+ b.brushPose = {
460
+ overrideAngle: brush.overridePoseAngle!,
461
+ overrideTiltX: brush.overridePoseTiltX!,
462
+ overrideTiltY: brush.overridePoseTiltY!,
463
+ overridePressure: brush.overridePosePressure!,
464
+ pressure: parsePercent(brush.brushPosePressure!),
465
+ tiltX: brush.brushPoseTiltX!,
466
+ tiltY: brush.brushPoseTiltY!,
467
+ angle: brush.brushPoseAngle!,
468
+ };
469
+ }
470
+
471
+ const to = brush.toolOptions;
472
+ if (to) {
473
+ b.toolOptions = {
474
+ brushPreset: to.brushPreset,
475
+ flow: to.flow ?? 100,
476
+ smooth: to.Smoo ?? 0,
477
+ mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
478
+ opacity: to.Opct ?? 100,
479
+ smoothing: !!to.smoothing,
480
+ smoothingValue: to.smoothingValue || 0,
481
+ smoothingRadiusMode: !!to.smoothingRadiusMode,
482
+ smoothingCatchup: !!to.smoothingCatchup,
483
+ smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
484
+ smoothingZoomCompensation: !!to.smoothingZoomCompensation,
485
+ pressureSmoothing: !!to.pressureSmoothing,
486
+ usePressureOverridesSize: !!to.usePressureOverridesSize,
487
+ usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
488
+ useLegacy: !!to.useLegacy,
489
+ };
490
+
491
+ if (to.prVr) {
492
+ b.toolOptions.flowDynamics = parseDynamics(to.prVr);
493
+ }
494
+
495
+ if (to.opVr) {
496
+ b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
497
+ }
498
+
499
+ if (to.szVr) {
500
+ b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
501
+ }
502
+ }
503
+
504
+ brushes.push(b);
505
+ }
506
+ break;
507
+ }
508
+ case 'patt': {
509
+ if (reader.offset < end) { // TODO: check multiple patterns
510
+ patterns.push(readPattern(reader));
511
+ reader.offset = end;
512
+ }
513
+ break;
514
+ }
515
+ case 'phry': {
516
+ // TODO: what is this ?
517
+ const desc: PhryDescriptor = readVersionAndDescriptor(reader);
518
+ if (options.logMissingFeatures) {
519
+ if (desc.hierarchy?.length) {
520
+ console.log('unhandled phry section', desc);
521
+ }
522
+ }
523
+ break;
524
+ }
525
+ default:
526
+ throw new Error(`Invalid brush type: ${type}`);
527
+ }
528
+
529
+ // align to 4 bytes
530
+ while (size % 4) {
531
+ reader.offset++;
532
+ size++;
533
+ }
534
+ }
535
+ } else {
536
+ throw new Error(`Unsupported ABR version (${version})`);
537
+ }
538
+
539
+ return { samples, patterns, brushes };
540
+ }