ag-psd 15.0.4 → 15.0.5

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.
@@ -0,0 +1,2688 @@
1
+ import { fromByteArray, toByteArray } from 'base64-js';
2
+ import { readEffects, writeEffects } from './effectsHelpers';
3
+ import { clamp, createEnum, layerColors, MOCK_HANDLERS } from './helpers';
4
+ import {
5
+ LayerAdditionalInfo, BezierPath, Psd, ReadOptions, BrightnessAdjustment, ExposureAdjustment, VibranceAdjustment,
6
+ ColorBalanceAdjustment, BlackAndWhiteAdjustment, PhotoFilterAdjustment, ChannelMixerChannel,
7
+ ChannelMixerAdjustment, PosterizeAdjustment, ThresholdAdjustment, GradientMapAdjustment, CMYK,
8
+ SelectiveColorAdjustment, ColorLookupAdjustment, LevelsAdjustmentChannel, LevelsAdjustment,
9
+ CurvesAdjustment, CurvesAdjustmentChannel, HueSaturationAdjustment, HueSaturationAdjustmentChannel,
10
+ PresetInfo, Color, ColorBalanceValues, WriteOptions, LinkedFile, PlacedLayerType, Warp, KeyDescriptorItem,
11
+ BooleanOperation, LayerEffectsInfo, Annotation, LayerVectorMask, AnimationFrame, Timeline,
12
+ } from './psd';
13
+ import {
14
+ PsdReader, readSignature, readUnicodeString, skipBytes, readUint32, readUint8, readFloat64, readUint16,
15
+ readBytes, readInt16, checkSignature, readFloat32, readFixedPointPath32, readSection, readColor, readInt32,
16
+ readPascalString, readUnicodeStringWithLength, readAsciiString, readPattern,
17
+ } from './psdReader';
18
+ import {
19
+ PsdWriter, writeZeros, writeSignature, writeBytes, writeUint32, writeUint16, writeFloat64, writeUint8,
20
+ writeInt16, writeFloat32, writeFixedPointPath32, writeUnicodeString, writeSection, writeUnicodeStringWithPadding,
21
+ writeColor, writePascalString, writeInt32,
22
+ } from './psdWriter';
23
+ import {
24
+ Annt, BlnM, DescriptorColor, DescriptorUnitsValue, parsePercent, parseUnits, parseUnitsOrNumber, QuiltWarpDescriptor,
25
+ strokeStyleLineAlignment, strokeStyleLineCapType, strokeStyleLineJoinType, TextDescriptor, textGridding,
26
+ unitsPercent, unitsValue, WarpDescriptor, warpStyle, writeVersionAndDescriptor,
27
+ readVersionAndDescriptor, StrokeDescriptor, Ornt, horzVrtcToXY, LmfxDescriptor, Lfx2Descriptor,
28
+ FrameListDescriptor, TimelineDescriptor, FrameDescriptor, xyToHorzVrtc, serializeEffects,
29
+ parseEffects, parseColor, serializeColor, serializeVectorContent, parseVectorContent, parseTrackList, serializeTrackList, FractionDescriptor,
30
+ } from './descriptor';
31
+ import { serializeEngineData, parseEngineData } from './engineData';
32
+ import { encodeEngineData, decodeEngineData } from './text';
33
+
34
+ export interface ExtendedWriteOptions extends WriteOptions {
35
+ layerIds: Set<number>;
36
+ layerToId: Map<any, number>;
37
+ }
38
+
39
+ type HasMethod = (target: LayerAdditionalInfo) => boolean;
40
+ type ReadMethod = (reader: PsdReader, target: LayerAdditionalInfo, left: () => number, psd: Psd, options: ReadOptions) => void;
41
+ type WriteMethod = (writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) => void;
42
+
43
+ export interface InfoHandler {
44
+ key: string;
45
+ has: HasMethod;
46
+ read: ReadMethod;
47
+ write: WriteMethod;
48
+ }
49
+
50
+ export const infoHandlers: InfoHandler[] = [];
51
+ export const infoHandlersMap: { [key: string]: InfoHandler; } = {};
52
+
53
+ function addHandler(key: string, has: HasMethod, read: ReadMethod, write: WriteMethod) {
54
+ const handler: InfoHandler = { key, has, read, write };
55
+ infoHandlers.push(handler);
56
+ infoHandlersMap[handler.key] = handler;
57
+ }
58
+
59
+ function addHandlerAlias(key: string, target: string) {
60
+ infoHandlersMap[key] = infoHandlersMap[target];
61
+ }
62
+
63
+ function hasKey(key: keyof LayerAdditionalInfo) {
64
+ return (target: LayerAdditionalInfo) => target[key] !== undefined;
65
+ }
66
+
67
+ function readLength64(reader: PsdReader) {
68
+ if (readUint32(reader)) throw new Error(`Resource size above 4 GB limit at ${reader.offset.toString(16)}`);
69
+ return readUint32(reader);
70
+ }
71
+
72
+ function writeLength64(writer: PsdWriter, length: number) {
73
+ writeUint32(writer, 0);
74
+ writeUint32(writer, length);
75
+ }
76
+
77
+ addHandler(
78
+ 'TySh',
79
+ hasKey('text'),
80
+ (reader, target, leftBytes) => {
81
+ if (readInt16(reader) !== 1) throw new Error(`Invalid TySh version`);
82
+
83
+ const transform: number[] = [];
84
+ for (let i = 0; i < 6; i++) transform.push(readFloat64(reader));
85
+
86
+ if (readInt16(reader) !== 50) throw new Error(`Invalid TySh text version`);
87
+ const text: TextDescriptor = readVersionAndDescriptor(reader);
88
+
89
+ if (readInt16(reader) !== 1) throw new Error(`Invalid TySh warp version`);
90
+ const warp: WarpDescriptor = readVersionAndDescriptor(reader);
91
+
92
+ target.text = {
93
+ transform,
94
+ left: readFloat32(reader),
95
+ top: readFloat32(reader),
96
+ right: readFloat32(reader),
97
+ bottom: readFloat32(reader),
98
+ text: text['Txt '].replace(/\r/g, '\n'),
99
+ index: text.TextIndex || 0,
100
+ gridding: textGridding.decode(text.textGridding),
101
+ antiAlias: Annt.decode(text.AntA),
102
+ orientation: Ornt.decode(text.Ornt),
103
+ warp: {
104
+ style: warpStyle.decode(warp.warpStyle),
105
+ value: warp.warpValue || 0,
106
+ perspective: warp.warpPerspective || 0,
107
+ perspectiveOther: warp.warpPerspectiveOther || 0,
108
+ rotate: Ornt.decode(warp.warpRotate),
109
+ },
110
+ };
111
+
112
+ if (text.EngineData) {
113
+ const engineData = decodeEngineData(parseEngineData(text.EngineData));
114
+
115
+ // const before = parseEngineData(text.EngineData);
116
+ // const after = encodeEngineData(engineData);
117
+ // require('fs').writeFileSync('before.txt', require('util').inspect(before, false, 99, false), 'utf8');
118
+ // require('fs').writeFileSync('after.txt', require('util').inspect(after, false, 99, false), 'utf8');
119
+
120
+ // console.log(require('util').inspect(parseEngineData(text.EngineData), false, 99, true));
121
+ target.text = { ...target.text, ...engineData };
122
+ // console.log(require('util').inspect(target.text, false, 99, true));
123
+ }
124
+
125
+ skipBytes(reader, leftBytes());
126
+ },
127
+ (writer, target) => {
128
+ const text = target.text!;
129
+ const warp = text.warp || {};
130
+ const transform = text.transform || [1, 0, 0, 1, 0, 0];
131
+
132
+ const textDescriptor: TextDescriptor = {
133
+ 'Txt ': (text.text || '').replace(/\r?\n/g, '\r'),
134
+ textGridding: textGridding.encode(text.gridding),
135
+ Ornt: Ornt.encode(text.orientation),
136
+ AntA: Annt.encode(text.antiAlias),
137
+ TextIndex: text.index || 0,
138
+ EngineData: serializeEngineData(encodeEngineData(text)),
139
+ };
140
+
141
+ writeInt16(writer, 1); // version
142
+
143
+ for (let i = 0; i < 6; i++) {
144
+ writeFloat64(writer, transform[i]);
145
+ }
146
+
147
+ writeInt16(writer, 50); // text version
148
+ writeVersionAndDescriptor(writer, '', 'TxLr', textDescriptor);
149
+
150
+ writeInt16(writer, 1); // warp version
151
+ writeVersionAndDescriptor(writer, '', 'warp', encodeWarp(warp));
152
+
153
+ writeFloat32(writer, text.left!);
154
+ writeFloat32(writer, text.top!);
155
+ writeFloat32(writer, text.right!);
156
+ writeFloat32(writer, text.bottom!);
157
+
158
+ // writeZeros(writer, 2);
159
+ },
160
+ );
161
+
162
+ // vector fills
163
+
164
+ addHandler(
165
+ 'SoCo',
166
+ target => target.vectorFill !== undefined && target.vectorStroke === undefined &&
167
+ target.vectorFill.type === 'color',
168
+ (reader, target) => {
169
+ const descriptor = readVersionAndDescriptor(reader);
170
+ target.vectorFill = parseVectorContent(descriptor);
171
+ },
172
+ (writer, target) => {
173
+ const { descriptor } = serializeVectorContent(target.vectorFill!);
174
+ writeVersionAndDescriptor(writer, '', 'null', descriptor);
175
+ },
176
+ );
177
+
178
+ addHandler(
179
+ 'GdFl',
180
+ target => target.vectorFill !== undefined && target.vectorStroke === undefined &&
181
+ (target.vectorFill.type === 'solid' || target.vectorFill.type === 'noise'),
182
+ (reader, target, left) => {
183
+ const descriptor = readVersionAndDescriptor(reader);
184
+ target.vectorFill = parseVectorContent(descriptor);
185
+ skipBytes(reader, left());
186
+ },
187
+ (writer, target) => {
188
+ const { descriptor } = serializeVectorContent(target.vectorFill!);
189
+ writeVersionAndDescriptor(writer, '', 'null', descriptor);
190
+ },
191
+ );
192
+
193
+ addHandler(
194
+ 'PtFl',
195
+ target => target.vectorFill !== undefined && target.vectorStroke === undefined &&
196
+ target.vectorFill.type === 'pattern',
197
+ (reader, target) => {
198
+ const descriptor = readVersionAndDescriptor(reader);
199
+ target.vectorFill = parseVectorContent(descriptor);
200
+ },
201
+ (writer, target) => {
202
+ const { descriptor } = serializeVectorContent(target.vectorFill!);
203
+ writeVersionAndDescriptor(writer, '', 'null', descriptor);
204
+ },
205
+ );
206
+
207
+ addHandler(
208
+ 'vscg',
209
+ target => target.vectorFill !== undefined && target.vectorStroke !== undefined,
210
+ (reader, target, left) => {
211
+ readSignature(reader); // key
212
+ const desc = readVersionAndDescriptor(reader);
213
+ target.vectorFill = parseVectorContent(desc);
214
+ skipBytes(reader, left());
215
+ },
216
+ (writer, target) => {
217
+ const { descriptor, key } = serializeVectorContent(target.vectorFill!);
218
+ writeSignature(writer, key);
219
+ writeVersionAndDescriptor(writer, '', 'null', descriptor);
220
+ },
221
+ );
222
+
223
+ export function readBezierKnot(reader: PsdReader, width: number, height: number) {
224
+ const y0 = readFixedPointPath32(reader) * height;
225
+ const x0 = readFixedPointPath32(reader) * width;
226
+ const y1 = readFixedPointPath32(reader) * height;
227
+ const x1 = readFixedPointPath32(reader) * width;
228
+ const y2 = readFixedPointPath32(reader) * height;
229
+ const x2 = readFixedPointPath32(reader) * width;
230
+ return [x0, y0, x1, y1, x2, y2];
231
+ }
232
+
233
+ function writeBezierKnot(writer: PsdWriter, points: number[], width: number, height: number) {
234
+ writeFixedPointPath32(writer, points[1] / height); // y0
235
+ writeFixedPointPath32(writer, points[0] / width); // x0
236
+ writeFixedPointPath32(writer, points[3] / height); // y1
237
+ writeFixedPointPath32(writer, points[2] / width); // x1
238
+ writeFixedPointPath32(writer, points[5] / height); // y2
239
+ writeFixedPointPath32(writer, points[4] / width); // x2
240
+ }
241
+
242
+ export const booleanOperations: BooleanOperation[] = ['exclude', 'combine', 'subtract', 'intersect'];
243
+
244
+ export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, width: number, height: number, size: number) {
245
+ const end = reader.offset + size;
246
+ const paths = vectorMask.paths;
247
+ let path: BezierPath | undefined = undefined;
248
+
249
+ while ((end - reader.offset) >= 26) {
250
+ const selector = readUint16(reader);
251
+
252
+ switch (selector) {
253
+ case 0: // Closed subpath length record
254
+ case 3: { // Open subpath length record
255
+ readUint16(reader); // count
256
+ const boolOp = readInt16(reader);
257
+ readUint16(reader); // always 1 ?
258
+ skipBytes(reader, 18);
259
+ // TODO: 'combine' here might be wrong
260
+ path = { open: selector === 3, operation: boolOp === -1 ? 'combine' : booleanOperations[boolOp], knots: [] };
261
+ paths.push(path);
262
+ break;
263
+ }
264
+ case 1: // Closed subpath Bezier knot, linked
265
+ case 2: // Closed subpath Bezier knot, unlinked
266
+ case 4: // Open subpath Bezier knot, linked
267
+ case 5: // Open subpath Bezier knot, unlinked
268
+ path!.knots.push({ linked: (selector === 1 || selector === 4), points: readBezierKnot(reader, width, height) });
269
+ break;
270
+ case 6: // Path fill rule record
271
+ skipBytes(reader, 24);
272
+ break;
273
+ case 7: { // Clipboard record
274
+ // TODO: check if these need to be multiplied by document size
275
+ const top = readFixedPointPath32(reader);
276
+ const left = readFixedPointPath32(reader);
277
+ const bottom = readFixedPointPath32(reader);
278
+ const right = readFixedPointPath32(reader);
279
+ const resolution = readFixedPointPath32(reader);
280
+ skipBytes(reader, 4);
281
+ vectorMask.clipboard = { top, left, bottom, right, resolution };
282
+ break;
283
+ }
284
+ case 8: // Initial fill rule record
285
+ vectorMask.fillStartsWithAllPixels = !!readUint16(reader);
286
+ skipBytes(reader, 22);
287
+ break;
288
+ default: throw new Error('Invalid vmsk section');
289
+ }
290
+ }
291
+
292
+ return paths;
293
+ }
294
+
295
+ addHandler(
296
+ 'vmsk',
297
+ hasKey('vectorMask'),
298
+ (reader, target, left, { width, height }) => {
299
+ if (readUint32(reader) !== 3) throw new Error('Invalid vmsk version');
300
+
301
+ target.vectorMask = { paths: [] };
302
+ const vectorMask = target.vectorMask;
303
+
304
+ const flags = readUint32(reader);
305
+ vectorMask.invert = (flags & 1) !== 0;
306
+ vectorMask.notLink = (flags & 2) !== 0;
307
+ vectorMask.disable = (flags & 4) !== 0;
308
+
309
+ readVectorMask(reader, vectorMask, width, height, left());
310
+
311
+ // drawBezierPaths(vectorMask.paths, width, height, 'out.png');
312
+
313
+ skipBytes(reader, left());
314
+ },
315
+ (writer, target, { width, height }) => {
316
+ const vectorMask = target.vectorMask!;
317
+ const flags =
318
+ (vectorMask.invert ? 1 : 0) |
319
+ (vectorMask.notLink ? 2 : 0) |
320
+ (vectorMask.disable ? 4 : 0);
321
+
322
+ writeUint32(writer, 3); // version
323
+ writeUint32(writer, flags);
324
+
325
+ // initial entry
326
+ writeUint16(writer, 6);
327
+ writeZeros(writer, 24);
328
+
329
+ const clipboard = vectorMask.clipboard;
330
+ if (clipboard) {
331
+ writeUint16(writer, 7);
332
+ writeFixedPointPath32(writer, clipboard.top);
333
+ writeFixedPointPath32(writer, clipboard.left);
334
+ writeFixedPointPath32(writer, clipboard.bottom);
335
+ writeFixedPointPath32(writer, clipboard.right);
336
+ writeFixedPointPath32(writer, clipboard.resolution);
337
+ writeZeros(writer, 4);
338
+ }
339
+
340
+ if (vectorMask.fillStartsWithAllPixels !== undefined) {
341
+ writeUint16(writer, 8);
342
+ writeUint16(writer, vectorMask.fillStartsWithAllPixels ? 1 : 0);
343
+ writeZeros(writer, 22);
344
+ }
345
+
346
+ for (const path of vectorMask.paths) {
347
+ writeUint16(writer, path.open ? 3 : 0);
348
+ writeUint16(writer, path.knots.length);
349
+ writeUint16(writer, Math.abs(booleanOperations.indexOf(path.operation))); // default to 1 if not found
350
+ writeUint16(writer, 1);
351
+ writeZeros(writer, 18); // TODO: these are sometimes non-zero
352
+
353
+ const linkedKnot = path.open ? 4 : 1;
354
+ const unlinkedKnot = path.open ? 5 : 2;
355
+
356
+ for (const { linked, points } of path.knots) {
357
+ writeUint16(writer, linked ? linkedKnot : unlinkedKnot);
358
+ writeBezierKnot(writer, points, width, height);
359
+ }
360
+ }
361
+ },
362
+ );
363
+
364
+ // TODO: need to write vmsk if has outline ?
365
+ addHandlerAlias('vsms', 'vmsk');
366
+ // addHandlerAlias('vmsk', 'vsms');
367
+
368
+ interface VogkDescriptor {
369
+ keyDescriptorList: {
370
+ keyShapeInvalidated?: boolean;
371
+ keyOriginType?: number;
372
+ keyOriginResolution?: number;
373
+ keyOriginRRectRadii?: {
374
+ unitValueQuadVersion: number;
375
+ topRight: DescriptorUnitsValue;
376
+ topLeft: DescriptorUnitsValue;
377
+ bottomLeft: DescriptorUnitsValue;
378
+ bottomRight: DescriptorUnitsValue;
379
+ };
380
+ keyOriginShapeBBox?: {
381
+ unitValueQuadVersion: number;
382
+ 'Top ': DescriptorUnitsValue;
383
+ Left: DescriptorUnitsValue;
384
+ Btom: DescriptorUnitsValue;
385
+ Rght: DescriptorUnitsValue;
386
+ };
387
+ keyOriginBoxCorners?: {
388
+ rectangleCornerA: { Hrzn: number; Vrtc: number; };
389
+ rectangleCornerB: { Hrzn: number; Vrtc: number; };
390
+ rectangleCornerC: { Hrzn: number; Vrtc: number; };
391
+ rectangleCornerD: { Hrzn: number; Vrtc: number; };
392
+ };
393
+ Trnf?: { xx: number; xy: number; yx: number; yy: number; tx: number; ty: number; },
394
+ keyOriginIndex: number;
395
+ }[];
396
+ }
397
+
398
+ addHandler(
399
+ 'vogk',
400
+ hasKey('vectorOrigination'),
401
+ (reader, target, left) => {
402
+ if (readInt32(reader) !== 1) throw new Error(`Invalid vogk version`);
403
+ const desc = readVersionAndDescriptor(reader) as VogkDescriptor;
404
+ // console.log(require('util').inspect(desc, false, 99, true));
405
+
406
+ target.vectorOrigination = { keyDescriptorList: [] };
407
+
408
+ for (const i of desc.keyDescriptorList) {
409
+ const item: KeyDescriptorItem = {};
410
+
411
+ if (i.keyShapeInvalidated != null) item.keyShapeInvalidated = i.keyShapeInvalidated;
412
+ if (i.keyOriginType != null) item.keyOriginType = i.keyOriginType;
413
+ if (i.keyOriginResolution != null) item.keyOriginResolution = i.keyOriginResolution;
414
+ if (i.keyOriginShapeBBox) {
415
+ item.keyOriginShapeBoundingBox = {
416
+ top: parseUnits(i.keyOriginShapeBBox['Top ']),
417
+ left: parseUnits(i.keyOriginShapeBBox.Left),
418
+ bottom: parseUnits(i.keyOriginShapeBBox.Btom),
419
+ right: parseUnits(i.keyOriginShapeBBox.Rght),
420
+ };
421
+ }
422
+ const rectRadii = i.keyOriginRRectRadii;
423
+ if (rectRadii) {
424
+ item.keyOriginRRectRadii = {
425
+ topRight: parseUnits(rectRadii.topRight),
426
+ topLeft: parseUnits(rectRadii.topLeft),
427
+ bottomLeft: parseUnits(rectRadii.bottomLeft),
428
+ bottomRight: parseUnits(rectRadii.bottomRight),
429
+ };
430
+ }
431
+ const corners = i.keyOriginBoxCorners;
432
+ if (corners) {
433
+ item.keyOriginBoxCorners = [
434
+ { x: corners.rectangleCornerA.Hrzn, y: corners.rectangleCornerA.Vrtc },
435
+ { x: corners.rectangleCornerB.Hrzn, y: corners.rectangleCornerB.Vrtc },
436
+ { x: corners.rectangleCornerC.Hrzn, y: corners.rectangleCornerC.Vrtc },
437
+ { x: corners.rectangleCornerD.Hrzn, y: corners.rectangleCornerD.Vrtc },
438
+ ];
439
+ }
440
+ const trnf = i.Trnf;
441
+ if (trnf) {
442
+ item.transform = [trnf.xx, trnf.xy, trnf.xy, trnf.yy, trnf.tx, trnf.ty];
443
+ }
444
+
445
+ target.vectorOrigination.keyDescriptorList.push(item);
446
+ }
447
+
448
+ skipBytes(reader, left());
449
+ },
450
+ (writer, target) => {
451
+ target;
452
+ const orig = target.vectorOrigination!;
453
+ const desc: VogkDescriptor = { keyDescriptorList: [] };
454
+
455
+ for (let i = 0; i < orig.keyDescriptorList.length; i++) {
456
+ const item = orig.keyDescriptorList[i];
457
+
458
+ if (item.keyShapeInvalidated) {
459
+ desc.keyDescriptorList.push({ keyShapeInvalidated: true, keyOriginIndex: i });
460
+ } else {
461
+ desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
462
+
463
+ const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1];
464
+
465
+ if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType;
466
+ if (item.keyOriginResolution != null) out.keyOriginResolution = item.keyOriginResolution;
467
+
468
+ const radii = item.keyOriginRRectRadii;
469
+ if (radii) {
470
+ out.keyOriginRRectRadii = {
471
+ unitValueQuadVersion: 1,
472
+ topRight: unitsValue(radii.topRight, 'topRight'),
473
+ topLeft: unitsValue(radii.topLeft, 'topLeft'),
474
+ bottomLeft: unitsValue(radii.bottomLeft, 'bottomLeft'),
475
+ bottomRight: unitsValue(radii.bottomRight, 'bottomRight'),
476
+ };
477
+ }
478
+
479
+ const box = item.keyOriginShapeBoundingBox;
480
+ if (box) {
481
+ out.keyOriginShapeBBox = {
482
+ unitValueQuadVersion: 1,
483
+ 'Top ': unitsValue(box.top, 'top'),
484
+ Left: unitsValue(box.left, 'left'),
485
+ Btom: unitsValue(box.bottom, 'bottom'),
486
+ Rght: unitsValue(box.right, 'right'),
487
+ };
488
+ }
489
+
490
+ const corners = item.keyOriginBoxCorners;
491
+ if (corners && corners.length === 4) {
492
+ out.keyOriginBoxCorners = {
493
+ rectangleCornerA: { Hrzn: corners[0].x, Vrtc: corners[0].y },
494
+ rectangleCornerB: { Hrzn: corners[1].x, Vrtc: corners[1].y },
495
+ rectangleCornerC: { Hrzn: corners[2].x, Vrtc: corners[2].y },
496
+ rectangleCornerD: { Hrzn: corners[3].x, Vrtc: corners[3].y },
497
+ };
498
+ }
499
+
500
+ const transform = item.transform;
501
+ if (transform && transform.length === 6) {
502
+ out.Trnf = {
503
+ xx: transform[0],
504
+ xy: transform[1],
505
+ yx: transform[2],
506
+ yy: transform[3],
507
+ tx: transform[4],
508
+ ty: transform[5],
509
+ };
510
+ }
511
+
512
+ out.keyOriginIndex = i;
513
+ }
514
+ }
515
+
516
+ writeInt32(writer, 1); // version
517
+ writeVersionAndDescriptor(writer, '', 'null', desc);
518
+ }
519
+ );
520
+
521
+ addHandler(
522
+ 'lmfx',
523
+ target => target.effects !== undefined && hasMultiEffects(target.effects),
524
+ (reader, target, left, _, options) => {
525
+ const version = readUint32(reader);
526
+ if (version !== 0) throw new Error('Invalid lmfx version');
527
+
528
+ const desc: LmfxDescriptor = readVersionAndDescriptor(reader);
529
+ // console.log(require('util').inspect(info, false, 99, true));
530
+
531
+ // discard if read in 'lrFX' or 'lfx2' section
532
+ target.effects = parseEffects(desc, !!options.logMissingFeatures);
533
+
534
+ skipBytes(reader, left());
535
+ },
536
+ (writer, target, _, options) => {
537
+ const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true);
538
+
539
+ writeUint32(writer, 0); // version
540
+ writeVersionAndDescriptor(writer, '', 'null', desc);
541
+ },
542
+ );
543
+
544
+ addHandler(
545
+ 'lrFX',
546
+ hasKey('effects'),
547
+ (reader, target, left) => {
548
+ if (!target.effects) target.effects = readEffects(reader);
549
+
550
+ skipBytes(reader, left());
551
+ },
552
+ (writer, target) => {
553
+ writeEffects(writer, target.effects!);
554
+ },
555
+ );
556
+
557
+ addHandler(
558
+ 'luni',
559
+ hasKey('name'),
560
+ (reader, target, left) => {
561
+ target.name = readUnicodeString(reader);
562
+ skipBytes(reader, left());
563
+ },
564
+ (writer, target) => {
565
+ writeUnicodeString(writer, target.name!);
566
+ // writeUint16(writer, 0); // padding (but not extending string length)
567
+ },
568
+ );
569
+
570
+ addHandler(
571
+ 'lnsr',
572
+ hasKey('nameSource'),
573
+ (reader, target) => target.nameSource = readSignature(reader),
574
+ (writer, target) => writeSignature(writer, target.nameSource!),
575
+ );
576
+
577
+ addHandler(
578
+ 'lyid',
579
+ hasKey('id'),
580
+ (reader, target) => target.id = readUint32(reader),
581
+ (writer, target, _psd, options) => {
582
+ let id = target.id!;
583
+ while (options.layerIds.has(id)) id += 100; // make sure we don't have duplicate layer ids
584
+ writeUint32(writer, id);
585
+ options.layerIds.add(id);
586
+ options.layerToId.set(target, id);
587
+ },
588
+ );
589
+
590
+ addHandler(
591
+ 'lsct',
592
+ hasKey('sectionDivider'),
593
+ (reader, target, left) => {
594
+ target.sectionDivider = { type: readUint32(reader) };
595
+
596
+ if (left()) {
597
+ checkSignature(reader, '8BIM');
598
+ target.sectionDivider.key = readSignature(reader);
599
+ }
600
+
601
+ if (left()) {
602
+ target.sectionDivider.subType = readUint32(reader);
603
+ }
604
+ },
605
+ (writer, target) => {
606
+ writeUint32(writer, target.sectionDivider!.type);
607
+
608
+ if (target.sectionDivider!.key) {
609
+ writeSignature(writer, '8BIM');
610
+ writeSignature(writer, target.sectionDivider!.key);
611
+
612
+ if (target.sectionDivider!.subType !== undefined) {
613
+ writeUint32(writer, target.sectionDivider!.subType);
614
+ }
615
+ }
616
+ },
617
+ );
618
+
619
+ // it seems lsdk is used when there's a layer is nested more than 6 levels, but I don't know why?
620
+ // maybe some limitation of old version of PS?
621
+ addHandlerAlias('lsdk', 'lsct');
622
+
623
+ addHandler(
624
+ 'clbl',
625
+ hasKey('blendClippendElements'),
626
+ (reader, target) => {
627
+ target.blendClippendElements = !!readUint8(reader);
628
+ skipBytes(reader, 3);
629
+ },
630
+ (writer, target) => {
631
+ writeUint8(writer, target.blendClippendElements ? 1 : 0);
632
+ writeZeros(writer, 3);
633
+ },
634
+ );
635
+
636
+ addHandler(
637
+ 'infx',
638
+ hasKey('blendInteriorElements'),
639
+ (reader, target) => {
640
+ target.blendInteriorElements = !!readUint8(reader);
641
+ skipBytes(reader, 3);
642
+ },
643
+ (writer, target) => {
644
+ writeUint8(writer, target.blendInteriorElements ? 1 : 0);
645
+ writeZeros(writer, 3);
646
+ },
647
+ );
648
+
649
+ addHandler(
650
+ 'knko',
651
+ hasKey('knockout'),
652
+ (reader, target) => {
653
+ target.knockout = !!readUint8(reader);
654
+ skipBytes(reader, 3);
655
+ },
656
+ (writer, target) => {
657
+ writeUint8(writer, target.knockout ? 1 : 0);
658
+ writeZeros(writer, 3);
659
+ },
660
+ );
661
+
662
+ addHandler(
663
+ 'lmgm',
664
+ hasKey('layerMaskAsGlobalMask'),
665
+ (reader, target) => {
666
+ target.layerMaskAsGlobalMask = !!readUint8(reader);
667
+ skipBytes(reader, 3);
668
+ },
669
+ (writer, target) => {
670
+ writeUint8(writer, target.layerMaskAsGlobalMask ? 1 : 0);
671
+ writeZeros(writer, 3);
672
+ },
673
+ );
674
+
675
+ addHandler(
676
+ 'lspf',
677
+ hasKey('protected'),
678
+ (reader, target) => {
679
+ const flags = readUint32(reader);
680
+ target.protected = {
681
+ transparency: (flags & 0x01) !== 0,
682
+ composite: (flags & 0x02) !== 0,
683
+ position: (flags & 0x04) !== 0,
684
+ };
685
+
686
+ if (flags & 0x08) target.protected.artboards = true;
687
+ },
688
+ (writer, target) => {
689
+ const flags =
690
+ (target.protected!.transparency ? 0x01 : 0) |
691
+ (target.protected!.composite ? 0x02 : 0) |
692
+ (target.protected!.position ? 0x04 : 0) |
693
+ (target.protected!.artboards ? 0x08 : 0);
694
+
695
+ writeUint32(writer, flags);
696
+ },
697
+ );
698
+
699
+ addHandler(
700
+ 'lclr',
701
+ hasKey('layerColor'),
702
+ (reader, target) => {
703
+ const color = readUint16(reader);
704
+ skipBytes(reader, 6);
705
+ target.layerColor = layerColors[color];
706
+ },
707
+ (writer, target) => {
708
+ const index = layerColors.indexOf(target.layerColor!);
709
+ writeUint16(writer, index === -1 ? 0 : index);
710
+ writeZeros(writer, 6);
711
+ },
712
+ );
713
+
714
+ interface CustomDescriptor {
715
+ layerTime?: number;
716
+ }
717
+
718
+ addHandler(
719
+ 'shmd',
720
+ target => target.timestamp !== undefined || target.animationFrames !== undefined ||
721
+ target.animationFrameFlags !== undefined || target.timeline !== undefined,
722
+ (reader, target, left, _, options) => {
723
+ const count = readUint32(reader);
724
+
725
+ for (let i = 0; i < count; i++) {
726
+ checkSignature(reader, '8BIM');
727
+ const key = readSignature(reader);
728
+ readUint8(reader); // copy
729
+ skipBytes(reader, 3);
730
+
731
+ readSection(reader, 1, left => {
732
+ if (key === 'cust') {
733
+ const desc = readVersionAndDescriptor(reader) as CustomDescriptor;
734
+ // console.log('cust', target.name, require('util').inspect(desc, false, 99, true));
735
+ if (desc.layerTime !== undefined) target.timestamp = desc.layerTime;
736
+ } else if (key === 'mlst') {
737
+ const desc = readVersionAndDescriptor(reader) as FrameListDescriptor;
738
+ // console.log('mlst', target.name, require('util').inspect(desc, false, 99, true));
739
+
740
+ target.animationFrames = [];
741
+
742
+ for (let i = 0; i < desc.LaSt.length; i++) {
743
+ const f = desc.LaSt[i];
744
+ const frame: AnimationFrame = { frames: f.FrLs };
745
+ if (f.enab !== undefined) frame.enable = f.enab;
746
+ if (f.Ofst) frame.offset = horzVrtcToXY(f.Ofst);
747
+ if (f.FXRf) frame.referencePoint = horzVrtcToXY(f.FXRf);
748
+ if (f.Lefx) frame.effects = parseEffects(f.Lefx, !!options.logMissingFeatures);
749
+ if (f.blendOptions && f.blendOptions.Opct) frame.opacity = parsePercent(f.blendOptions.Opct);
750
+ target.animationFrames.push(frame);
751
+ }
752
+ } else if (key === 'mdyn') {
753
+ // frame flags
754
+ readUint16(reader); // unknown
755
+ const propagate = readUint8(reader);
756
+ const flags = readUint8(reader);
757
+
758
+ target.animationFrameFlags = {
759
+ propagateFrameOne: !propagate,
760
+ unifyLayerPosition: (flags & 1) !== 0,
761
+ unifyLayerStyle: (flags & 2) !== 0,
762
+ unifyLayerVisibility: (flags & 4) !== 0,
763
+ };
764
+ } else if (key === 'tmln') {
765
+ const desc = readVersionAndDescriptor(reader) as TimelineDescriptor;
766
+ const timeScope = desc.timeScope;
767
+ // console.log('tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
768
+
769
+ const timeline: Timeline = {
770
+ start: timeScope.Strt,
771
+ duration: timeScope.duration,
772
+ inTime: timeScope.inTime,
773
+ outTime: timeScope.outTime,
774
+ autoScope: desc.autoScope,
775
+ audioLevel: desc.audioLevel,
776
+ };
777
+
778
+ if (desc.trackList) {
779
+ timeline.tracks = parseTrackList(desc.trackList, !!options.logMissingFeatures);
780
+ }
781
+
782
+ target.timeline = timeline;
783
+ // console.log('tmln:result', target.name, target.id, require('util').inspect(timeline, false, 99, true));
784
+ } else {
785
+ options.logDevFeatures && console.log('Unhandled "shmd" section key', key);
786
+ }
787
+
788
+ skipBytes(reader, left());
789
+ });
790
+ }
791
+
792
+ skipBytes(reader, left());
793
+ },
794
+ (writer, target, _, options) => {
795
+ const { animationFrames, animationFrameFlags, timestamp, timeline } = target;
796
+
797
+ let count = 0;
798
+ if (animationFrames) count++;
799
+ if (animationFrameFlags) count++;
800
+ if (timeline) count++;
801
+ if (timestamp !== undefined) count++;
802
+ writeUint32(writer, count);
803
+
804
+ if (animationFrames) {
805
+ writeSignature(writer, '8BIM');
806
+ writeSignature(writer, 'mlst');
807
+ writeUint8(writer, 0); // copy (always false)
808
+ writeZeros(writer, 3);
809
+ writeSection(writer, 2, () => {
810
+ const desc: FrameListDescriptor = {
811
+ LaID: target.id ?? 0,
812
+ LaSt: [],
813
+ };
814
+
815
+ for (let i = 0; i < animationFrames.length; i++) {
816
+ const f = animationFrames[i];
817
+ const frame: FrameDescriptor = {} as any;
818
+ if (f.enable !== undefined) frame.enab = f.enable;
819
+ frame.FrLs = f.frames;
820
+ if (f.offset) frame.Ofst = xyToHorzVrtc(f.offset);
821
+ if (f.referencePoint) frame.FXRf = xyToHorzVrtc(f.referencePoint);
822
+ if (f.effects) frame.Lefx = serializeEffects(f.effects, false, false);
823
+ if (f.opacity !== undefined) frame.blendOptions = { Opct: unitsPercent(f.opacity) };
824
+ desc.LaSt.push(frame);
825
+ }
826
+
827
+ writeVersionAndDescriptor(writer, '', 'null', desc);
828
+ }, true);
829
+ }
830
+
831
+ if (animationFrameFlags) {
832
+ writeSignature(writer, '8BIM');
833
+ writeSignature(writer, 'mdyn');
834
+ writeUint8(writer, 0); // copy (always false)
835
+ writeZeros(writer, 3);
836
+ writeSection(writer, 2, () => {
837
+ writeUint16(writer, 0); // unknown
838
+ writeUint8(writer, animationFrameFlags.propagateFrameOne ? 0x0 : 0xf);
839
+ writeUint8(writer,
840
+ (animationFrameFlags.unifyLayerPosition ? 1 : 0) |
841
+ (animationFrameFlags.unifyLayerStyle ? 2 : 0) |
842
+ (animationFrameFlags.unifyLayerVisibility ? 4 : 0));
843
+ });
844
+ }
845
+
846
+ if (timeline) {
847
+ writeSignature(writer, '8BIM');
848
+ writeSignature(writer, 'tmln');
849
+ writeUint8(writer, 0); // copy (always false)
850
+ writeZeros(writer, 3);
851
+ writeSection(writer, 2, () => {
852
+ const desc: TimelineDescriptor = {
853
+ Vrsn: 1,
854
+ timeScope: {
855
+ Vrsn: 1,
856
+ Strt: timeline.start,
857
+ duration: timeline.duration,
858
+ inTime: timeline.inTime,
859
+ outTime: timeline.outTime,
860
+ },
861
+ autoScope: timeline.autoScope,
862
+ audioLevel: timeline.audioLevel,
863
+ } as any;
864
+
865
+ if (timeline.tracks) {
866
+ desc.trackList = serializeTrackList(timeline.tracks);
867
+ }
868
+
869
+ const id = options.layerToId.get(target) || target.id || 0;
870
+ if (!id) throw new Error('You need to provide layer.id value whan writing document with animations');
871
+ desc.LyrI = id;
872
+
873
+ // console.log('WRITE:tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
874
+ writeVersionAndDescriptor(writer, '', 'null', desc, 'anim');
875
+ }, true);
876
+ }
877
+
878
+ if (timestamp !== undefined) {
879
+ writeSignature(writer, '8BIM');
880
+ writeSignature(writer, 'cust');
881
+ writeUint8(writer, 0); // copy (always false)
882
+ writeZeros(writer, 3);
883
+ writeSection(writer, 2, () => {
884
+ const desc: CustomDescriptor = {
885
+ layerTime: timestamp,
886
+ };
887
+ writeVersionAndDescriptor(writer, '', 'metadata', desc);
888
+ }, true);
889
+ }
890
+ },
891
+ );
892
+
893
+ addHandler(
894
+ 'vstk',
895
+ hasKey('vectorStroke'),
896
+ (reader, target, left) => {
897
+ const desc = readVersionAndDescriptor(reader) as StrokeDescriptor;
898
+ // console.log(require('util').inspect(desc, false, 99, true));
899
+
900
+ target.vectorStroke = {
901
+ strokeEnabled: desc.strokeEnabled,
902
+ fillEnabled: desc.fillEnabled,
903
+ lineWidth: parseUnits(desc.strokeStyleLineWidth),
904
+ lineDashOffset: parseUnits(desc.strokeStyleLineDashOffset),
905
+ miterLimit: desc.strokeStyleMiterLimit,
906
+ lineCapType: strokeStyleLineCapType.decode(desc.strokeStyleLineCapType),
907
+ lineJoinType: strokeStyleLineJoinType.decode(desc.strokeStyleLineJoinType),
908
+ lineAlignment: strokeStyleLineAlignment.decode(desc.strokeStyleLineAlignment),
909
+ scaleLock: desc.strokeStyleScaleLock,
910
+ strokeAdjust: desc.strokeStyleStrokeAdjust,
911
+ lineDashSet: desc.strokeStyleLineDashSet.map(parseUnits),
912
+ blendMode: BlnM.decode(desc.strokeStyleBlendMode),
913
+ opacity: parsePercent(desc.strokeStyleOpacity),
914
+ content: parseVectorContent(desc.strokeStyleContent),
915
+ resolution: desc.strokeStyleResolution,
916
+ };
917
+
918
+ skipBytes(reader, left());
919
+ },
920
+ (writer, target) => {
921
+ const stroke = target.vectorStroke!;
922
+ const descriptor: StrokeDescriptor = {
923
+ strokeStyleVersion: 2,
924
+ strokeEnabled: !!stroke.strokeEnabled,
925
+ fillEnabled: !!stroke.fillEnabled,
926
+ strokeStyleLineWidth: stroke.lineWidth || { value: 3, units: 'Points' },
927
+ strokeStyleLineDashOffset: stroke.lineDashOffset || { value: 0, units: 'Points' },
928
+ strokeStyleMiterLimit: stroke.miterLimit ?? 100,
929
+ strokeStyleLineCapType: strokeStyleLineCapType.encode(stroke.lineCapType),
930
+ strokeStyleLineJoinType: strokeStyleLineJoinType.encode(stroke.lineJoinType),
931
+ strokeStyleLineAlignment: strokeStyleLineAlignment.encode(stroke.lineAlignment),
932
+ strokeStyleScaleLock: !!stroke.scaleLock,
933
+ strokeStyleStrokeAdjust: !!stroke.strokeAdjust,
934
+ strokeStyleLineDashSet: stroke.lineDashSet || [],
935
+ strokeStyleBlendMode: BlnM.encode(stroke.blendMode),
936
+ strokeStyleOpacity: unitsPercent(stroke.opacity ?? 1),
937
+ strokeStyleContent: serializeVectorContent(
938
+ stroke.content || { type: 'color', color: { r: 0, g: 0, b: 0 } }).descriptor,
939
+ strokeStyleResolution: stroke.resolution ?? 72,
940
+ };
941
+
942
+ writeVersionAndDescriptor(writer, '', 'strokeStyle', descriptor);
943
+ },
944
+ );
945
+
946
+ interface ArtbDescriptor {
947
+ artboardRect: { 'Top ': number; Left: number; Btom: number; Rght: number; };
948
+ guideIndeces: any[];
949
+ artboardPresetName: string;
950
+ 'Clr ': DescriptorColor;
951
+ artboardBackgroundType: number;
952
+ }
953
+
954
+ addHandler(
955
+ 'artb', // per-layer arboard info
956
+ hasKey('artboard'),
957
+ (reader, target, left) => {
958
+ const desc = readVersionAndDescriptor(reader) as ArtbDescriptor;
959
+ const rect = desc.artboardRect;
960
+ target.artboard = {
961
+ rect: { top: rect['Top '], left: rect.Left, bottom: rect.Btom, right: rect.Rght },
962
+ guideIndices: desc.guideIndeces,
963
+ presetName: desc.artboardPresetName,
964
+ color: parseColor(desc['Clr ']),
965
+ backgroundType: desc.artboardBackgroundType,
966
+ };
967
+
968
+ skipBytes(reader, left());
969
+ },
970
+ (writer, target) => {
971
+ const artboard = target.artboard!;
972
+ const rect = artboard.rect;
973
+ const desc: ArtbDescriptor = {
974
+ artboardRect: { 'Top ': rect.top, Left: rect.left, Btom: rect.bottom, Rght: rect.right },
975
+ guideIndeces: artboard.guideIndices || [],
976
+ artboardPresetName: artboard.presetName || '',
977
+ 'Clr ': serializeColor(artboard.color),
978
+ artboardBackgroundType: artboard.backgroundType ?? 1,
979
+ };
980
+
981
+ writeVersionAndDescriptor(writer, '', 'artboard', desc);
982
+ },
983
+ );
984
+
985
+ addHandler(
986
+ 'sn2P',
987
+ hasKey('usingAlignedRendering'),
988
+ (reader, target) => target.usingAlignedRendering = !!readUint32(reader),
989
+ (writer, target) => writeUint32(writer, target.usingAlignedRendering ? 1 : 0),
990
+ );
991
+
992
+ const placedLayerTypes: PlacedLayerType[] = ['unknown', 'vector', 'raster', 'image stack'];
993
+
994
+ function parseWarp(warp: WarpDescriptor & QuiltWarpDescriptor): Warp {
995
+ const result: Warp = {
996
+ style: warpStyle.decode(warp.warpStyle),
997
+ ...(warp.warpValues ? { values: warp.warpValues } : { value: warp.warpValue || 0 }),
998
+ perspective: warp.warpPerspective || 0,
999
+ perspectiveOther: warp.warpPerspectiveOther || 0,
1000
+ rotate: Ornt.decode(warp.warpRotate),
1001
+ bounds: warp.bounds && {
1002
+ top: parseUnitsOrNumber(warp.bounds['Top ']),
1003
+ left: parseUnitsOrNumber(warp.bounds.Left),
1004
+ bottom: parseUnitsOrNumber(warp.bounds.Btom),
1005
+ right: parseUnitsOrNumber(warp.bounds.Rght),
1006
+ },
1007
+ uOrder: warp.uOrder,
1008
+ vOrder: warp.vOrder,
1009
+ };
1010
+
1011
+ if (warp.deformNumRows != null || warp.deformNumCols != null) {
1012
+ result.deformNumRows = warp.deformNumRows;
1013
+ result.deformNumCols = warp.deformNumCols;
1014
+ }
1015
+
1016
+ const envelopeWarp = warp.customEnvelopeWarp;
1017
+ if (envelopeWarp) {
1018
+ result.customEnvelopeWarp = {
1019
+ meshPoints: [],
1020
+ };
1021
+
1022
+ const xs = envelopeWarp.meshPoints.find(i => i.type === 'Hrzn')?.values || [];
1023
+ const ys = envelopeWarp.meshPoints.find(i => i.type === 'Vrtc')?.values || [];
1024
+
1025
+ for (let i = 0; i < xs.length; i++) {
1026
+ result.customEnvelopeWarp!.meshPoints.push({ x: xs[i], y: ys[i] });
1027
+ }
1028
+
1029
+ if (envelopeWarp.quiltSliceX || envelopeWarp.quiltSliceY) {
1030
+ result.customEnvelopeWarp.quiltSliceX = envelopeWarp.quiltSliceX?.[0]?.values || [];
1031
+ result.customEnvelopeWarp.quiltSliceY = envelopeWarp.quiltSliceY?.[0]?.values || [];
1032
+ }
1033
+ }
1034
+
1035
+ return result;
1036
+ }
1037
+
1038
+ function isQuiltWarp(warp: Warp) {
1039
+ return warp.deformNumCols != null || warp.deformNumRows != null ||
1040
+ warp.customEnvelopeWarp?.quiltSliceX || warp.customEnvelopeWarp?.quiltSliceY;
1041
+ }
1042
+
1043
+ function encodeWarp(warp: Warp): WarpDescriptor {
1044
+ const bounds = warp.bounds;
1045
+ const desc: WarpDescriptor = {
1046
+ warpStyle: warpStyle.encode(warp.style),
1047
+ ...(warp.values ? { warpValues: warp.values } : { warpValue: warp.value }),
1048
+ warpPerspective: warp.perspective || 0,
1049
+ warpPerspectiveOther: warp.perspectiveOther || 0,
1050
+ warpRotate: Ornt.encode(warp.rotate),
1051
+ bounds: {
1052
+ 'Top ': unitsValue(bounds && bounds.top || { units: 'Pixels', value: 0 }, 'bounds.top'),
1053
+ Left: unitsValue(bounds && bounds.left || { units: 'Pixels', value: 0 }, 'bounds.left'),
1054
+ Btom: unitsValue(bounds && bounds.bottom || { units: 'Pixels', value: 0 }, 'bounds.bottom'),
1055
+ Rght: unitsValue(bounds && bounds.right || { units: 'Pixels', value: 0 }, 'bounds.right'),
1056
+ },
1057
+ uOrder: warp.uOrder || 0,
1058
+ vOrder: warp.vOrder || 0,
1059
+ };
1060
+
1061
+ const isQuilt = isQuiltWarp(warp);
1062
+
1063
+ if (isQuilt) {
1064
+ const desc2 = desc as QuiltWarpDescriptor;
1065
+ desc2.deformNumRows = warp.deformNumRows || 0;
1066
+ desc2.deformNumCols = warp.deformNumCols || 0;
1067
+ }
1068
+
1069
+ const customEnvelopeWarp = warp.customEnvelopeWarp;
1070
+ if (customEnvelopeWarp) {
1071
+ const meshPoints = customEnvelopeWarp.meshPoints || [];
1072
+
1073
+ if (isQuilt) {
1074
+ const desc2 = desc as QuiltWarpDescriptor;
1075
+ desc2.customEnvelopeWarp = {
1076
+ quiltSliceX: [{
1077
+ type: 'quiltSliceX',
1078
+ values: customEnvelopeWarp.quiltSliceX || [],
1079
+ }],
1080
+ quiltSliceY: [{
1081
+ type: 'quiltSliceY',
1082
+ values: customEnvelopeWarp.quiltSliceY || [],
1083
+ }],
1084
+ meshPoints: [
1085
+ { type: 'Hrzn', values: meshPoints.map(p => p.x) },
1086
+ { type: 'Vrtc', values: meshPoints.map(p => p.y) },
1087
+ ],
1088
+ };
1089
+ } else {
1090
+ desc.customEnvelopeWarp = {
1091
+ meshPoints: [
1092
+ { type: 'Hrzn', values: meshPoints.map(p => p.x) },
1093
+ { type: 'Vrtc', values: meshPoints.map(p => p.y) },
1094
+ ],
1095
+ };
1096
+ }
1097
+ }
1098
+
1099
+ return desc;
1100
+ }
1101
+
1102
+ addHandler(
1103
+ 'PlLd',
1104
+ hasKey('placedLayer'),
1105
+ (reader, target, left) => {
1106
+ if (readSignature(reader) !== 'plcL') throw new Error(`Invalid PlLd signature`);
1107
+ if (readInt32(reader) !== 3) throw new Error(`Invalid PlLd version`);
1108
+ const id = readPascalString(reader, 1);
1109
+ const pageNumber = readInt32(reader);
1110
+ const totalPages = readInt32(reader); // TODO: check how this works ?
1111
+ readInt32(reader); // anitAliasPolicy 16
1112
+ const placedLayerType = readInt32(reader); // 0 = unknown, 1 = vector, 2 = raster, 3 = image stack
1113
+ if (!placedLayerTypes[placedLayerType]) throw new Error('Invalid PlLd type');
1114
+ const transform: number[] = [];
1115
+ for (let i = 0; i < 8; i++) transform.push(readFloat64(reader)); // x, y of 4 corners of the transform
1116
+ const warpVersion = readInt32(reader);
1117
+ if (warpVersion !== 0) throw new Error(`Invalid Warp version ${warpVersion}`);
1118
+ const warp: WarpDescriptor & QuiltWarpDescriptor = readVersionAndDescriptor(reader);
1119
+
1120
+ target.placedLayer = target.placedLayer || { // skip if SoLd already set it
1121
+ id,
1122
+ type: placedLayerTypes[placedLayerType],
1123
+ pageNumber,
1124
+ totalPages,
1125
+ transform,
1126
+ warp: parseWarp(warp),
1127
+ };
1128
+
1129
+ // console.log('PlLd warp', require('util').inspect(warp, false, 99, true));
1130
+ // console.log('PlLd', require('util').inspect(target.placedLayer, false, 99, true));
1131
+
1132
+ skipBytes(reader, left());
1133
+ },
1134
+ (writer, target) => {
1135
+ const placed = target.placedLayer!;
1136
+ writeSignature(writer, 'plcL');
1137
+ writeInt32(writer, 3); // version
1138
+ writePascalString(writer, placed.id, 1);
1139
+ writeInt32(writer, 1); // pageNumber
1140
+ writeInt32(writer, 1); // totalPages
1141
+ writeInt32(writer, 16); // anitAliasPolicy
1142
+ if (placedLayerTypes.indexOf(placed.type) === -1) throw new Error('Invalid placedLayer type');
1143
+ writeInt32(writer, placedLayerTypes.indexOf(placed.type));
1144
+ for (let i = 0; i < 8; i++) writeFloat64(writer, placed.transform[i]);
1145
+ writeInt32(writer, 0); // warp version
1146
+ const isQuilt = placed.warp && isQuiltWarp(placed.warp);
1147
+ const type = isQuilt ? 'quiltWarp' : 'warp';
1148
+ writeVersionAndDescriptor(writer, '', type, encodeWarp(placed.warp || {}), type);
1149
+ },
1150
+ );
1151
+
1152
+ interface SoLdDescriptor {
1153
+ Idnt: string;
1154
+ placed: string;
1155
+ PgNm: number;
1156
+ totalPages: number;
1157
+ Crop?: number;
1158
+ frameStep: FractionDescriptor;
1159
+ duration: FractionDescriptor;
1160
+ frameCount: number;
1161
+ Annt: number;
1162
+ Type: number;
1163
+ Trnf: number[];
1164
+ nonAffineTransform: number[];
1165
+ quiltWarp?: QuiltWarpDescriptor;
1166
+ warp: WarpDescriptor;
1167
+ 'Sz ': { Wdth: number; Hght: number; };
1168
+ Rslt: DescriptorUnitsValue;
1169
+ comp?: number;
1170
+ compInfo?: { compID: number; originalCompID: number; };
1171
+ }
1172
+
1173
+ addHandler(
1174
+ 'SoLd',
1175
+ hasKey('placedLayer'),
1176
+ (reader, target, left) => {
1177
+ if (readSignature(reader) !== 'soLD') throw new Error(`Invalid SoLd type`);
1178
+ if (readInt32(reader) !== 4) throw new Error(`Invalid SoLd version`);
1179
+ const desc: SoLdDescriptor = readVersionAndDescriptor(reader);
1180
+ // console.log('SoLd', require('util').inspect(desc, false, 99, true));
1181
+ // console.log('SoLd.warp', require('util').inspect(desc.warp, false, 99, true));
1182
+ // console.log('SoLd.quiltWarp', require('util').inspect(desc.quiltWarp, false, 99, true));
1183
+
1184
+ target.placedLayer = {
1185
+ id: desc.Idnt,
1186
+ placed: desc.placed,
1187
+ type: placedLayerTypes[desc.Type],
1188
+ pageNumber: desc.PgNm,
1189
+ totalPages: desc.totalPages,
1190
+ frameStep: desc.frameStep,
1191
+ duration: desc.duration,
1192
+ frameCount: desc.frameCount,
1193
+ transform: desc.Trnf,
1194
+ width: desc['Sz '].Wdth,
1195
+ height: desc['Sz '].Hght,
1196
+ resolution: parseUnits(desc.Rslt),
1197
+ warp: parseWarp((desc.quiltWarp || desc.warp) as any),
1198
+ };
1199
+
1200
+ if (desc.nonAffineTransform && desc.nonAffineTransform.some((x, i) => x !== desc.Trnf[i])) {
1201
+ target.placedLayer.nonAffineTransform = desc.nonAffineTransform;
1202
+ }
1203
+
1204
+ if (desc.Crop) target.placedLayer.crop = desc.Crop;
1205
+ if (desc.comp) target.placedLayer.comp = desc.comp;
1206
+ if (desc.compInfo) target.placedLayer.compInfo = desc.compInfo;
1207
+
1208
+ skipBytes(reader, left()); // HACK
1209
+ },
1210
+ (writer, target) => {
1211
+ writeSignature(writer, 'soLD');
1212
+ writeInt32(writer, 4); // version
1213
+
1214
+ const placed = target.placedLayer!;
1215
+ const desc: SoLdDescriptor = {
1216
+ Idnt: placed.id,
1217
+ placed: placed.placed ?? placed.id,
1218
+ PgNm: placed.pageNumber || 1,
1219
+ totalPages: placed.totalPages || 1,
1220
+ ...(placed.crop ? { Crop: placed.crop } : {}),
1221
+ frameStep: placed.frameStep || { numerator: 0, denominator: 600 },
1222
+ duration: placed.duration || { numerator: 0, denominator: 600 },
1223
+ frameCount: placed.frameCount || 0,
1224
+ Annt: 16,
1225
+ Type: placedLayerTypes.indexOf(placed.type),
1226
+ Trnf: placed.transform,
1227
+ nonAffineTransform: placed.nonAffineTransform ?? placed.transform,
1228
+ quiltWarp: {} as any,
1229
+ warp: encodeWarp(placed.warp || {}),
1230
+ 'Sz ': {
1231
+ Wdth: placed.width || 0, // TODO: find size ?
1232
+ Hght: placed.height || 0, // TODO: find size ?
1233
+ },
1234
+ Rslt: placed.resolution ? unitsValue(placed.resolution, 'resolution') : { units: 'Density', value: 72 },
1235
+ };
1236
+
1237
+ if (placed.warp && isQuiltWarp(placed.warp)) {
1238
+ const quiltWarp = encodeWarp(placed.warp) as QuiltWarpDescriptor;
1239
+ desc.quiltWarp = quiltWarp;
1240
+ desc.warp = {
1241
+ warpStyle: 'warpStyle.warpNone',
1242
+ warpValue: quiltWarp.warpValue,
1243
+ warpPerspective: quiltWarp.warpPerspective,
1244
+ warpPerspectiveOther: quiltWarp.warpPerspectiveOther,
1245
+ warpRotate: quiltWarp.warpRotate,
1246
+ bounds: quiltWarp.bounds,
1247
+ uOrder: quiltWarp.uOrder,
1248
+ vOrder: quiltWarp.vOrder,
1249
+ };
1250
+ } else {
1251
+ delete desc.quiltWarp;
1252
+ }
1253
+
1254
+ if (placed.comp) desc.comp = placed.comp;
1255
+ if (placed.compInfo) desc.compInfo = placed.compInfo;
1256
+
1257
+ writeVersionAndDescriptor(writer, '', 'null', desc, desc.quiltWarp ? 'quiltWarp' : 'warp');
1258
+ },
1259
+ );
1260
+
1261
+ addHandler(
1262
+ 'fxrp',
1263
+ hasKey('referencePoint'),
1264
+ (reader, target) => {
1265
+ target.referencePoint = {
1266
+ x: readFloat64(reader),
1267
+ y: readFloat64(reader),
1268
+ };
1269
+ },
1270
+ (writer, target) => {
1271
+ writeFloat64(writer, target.referencePoint!.x);
1272
+ writeFloat64(writer, target.referencePoint!.y);
1273
+ },
1274
+ );
1275
+
1276
+ if (MOCK_HANDLERS) {
1277
+ addHandler(
1278
+ 'Patt',
1279
+ target => (target as any)._Patt !== undefined,
1280
+ (reader, target, left) => {
1281
+ // console.log('additional info: Patt');
1282
+ (target as any)._Patt = readBytes(reader, left());
1283
+ },
1284
+ (writer, target) => false && writeBytes(writer, (target as any)._Patt),
1285
+ );
1286
+ } else {
1287
+ addHandler(
1288
+ 'Patt', // TODO: handle also Pat2 & Pat3
1289
+ target => !target,
1290
+ (reader, target, left) => {
1291
+ if (!left()) return;
1292
+
1293
+ skipBytes(reader, left()); return; // not supported yet
1294
+ target; readPattern;
1295
+
1296
+ // if (!target.patterns) target.patterns = [];
1297
+ // target.patterns.push(readPattern(reader));
1298
+ // skipBytes(reader, left());
1299
+ },
1300
+ (_writer, _target) => {
1301
+ },
1302
+ );
1303
+ }
1304
+
1305
+ function readRect(reader: PsdReader) {
1306
+ const top = readInt32(reader);
1307
+ const left = readInt32(reader);
1308
+ const bottom = readInt32(reader);
1309
+ const right = readInt32(reader);
1310
+ return { top, left, bottom, right };
1311
+ }
1312
+
1313
+ function writeRect(writer: PsdWriter, rect: { left: number; top: number; right: number; bottom: number }) {
1314
+ writeInt32(writer, rect.top);
1315
+ writeInt32(writer, rect.left);
1316
+ writeInt32(writer, rect.bottom);
1317
+ writeInt32(writer, rect.right);
1318
+ }
1319
+
1320
+ addHandler(
1321
+ 'Anno',
1322
+ target => (target as Psd).annotations !== undefined,
1323
+ (reader, target, left) => {
1324
+ const major = readUint16(reader);
1325
+ const minor = readUint16(reader);
1326
+ if (major !== 2 || minor !== 1) throw new Error('Invalid Anno version');
1327
+ const count = readUint32(reader);
1328
+ const annotations: Annotation[] = [];
1329
+
1330
+ for (let i = 0; i < count; i++) {
1331
+ /*const length =*/ readUint32(reader);
1332
+ const type = readSignature(reader);
1333
+ const open = !!readUint8(reader);
1334
+ /*const flags =*/ readUint8(reader); // always 28
1335
+ /*const optionalBlocks =*/ readUint16(reader);
1336
+ const iconLocation = readRect(reader);
1337
+ const popupLocation = readRect(reader);
1338
+ const color = readColor(reader);
1339
+ const author = readPascalString(reader, 2);
1340
+ const name = readPascalString(reader, 2);
1341
+ const date = readPascalString(reader, 2);
1342
+ /*const contentLength =*/ readUint32(reader);
1343
+ /*const dataType =*/ readSignature(reader);
1344
+ const dataLength = readUint32(reader);
1345
+ let data: string | Uint8Array;
1346
+
1347
+ if (type === 'txtA') {
1348
+ if (dataLength >= 2 && readUint16(reader) === 0xfeff) {
1349
+ data = readUnicodeStringWithLength(reader, (dataLength - 2) / 2);
1350
+ } else {
1351
+ reader.offset -= 2;
1352
+ data = readAsciiString(reader, dataLength);
1353
+ }
1354
+
1355
+ data = data.replace(/\r/g, '\n');
1356
+ } else if (type === 'sndA') {
1357
+ data = readBytes(reader, dataLength);
1358
+ } else {
1359
+ throw new Error('Unknown annotation type');
1360
+ }
1361
+
1362
+ annotations.push({
1363
+ type: type === 'txtA' ? 'text' : 'sound', open, iconLocation, popupLocation, color, author, name, date, data,
1364
+ });
1365
+ }
1366
+
1367
+ (target as Psd).annotations = annotations;
1368
+ skipBytes(reader, left());
1369
+ },
1370
+ (writer, target) => {
1371
+ const annotations = (target as Psd).annotations!;
1372
+
1373
+ writeUint16(writer, 2);
1374
+ writeUint16(writer, 1);
1375
+ writeUint32(writer, annotations.length);
1376
+
1377
+ for (const annotation of annotations) {
1378
+ const sound = annotation.type === 'sound';
1379
+
1380
+ if (sound && !(annotation.data instanceof Uint8Array)) throw new Error('Sound annotation data should be Uint8Array');
1381
+ if (!sound && typeof annotation.data !== 'string') throw new Error('Text annotation data should be string');
1382
+
1383
+ const lengthOffset = writer.offset;
1384
+ writeUint32(writer, 0); // length
1385
+ writeSignature(writer, sound ? 'sndA' : 'txtA');
1386
+ writeUint8(writer, annotation.open ? 1 : 0);
1387
+ writeUint8(writer, 28);
1388
+ writeUint16(writer, 1);
1389
+ writeRect(writer, annotation.iconLocation);
1390
+ writeRect(writer, annotation.popupLocation);
1391
+ writeColor(writer, annotation.color);
1392
+ writePascalString(writer, annotation.author || '', 2);
1393
+ writePascalString(writer, annotation.name || '', 2);
1394
+ writePascalString(writer, annotation.date || '', 2);
1395
+ const contentOffset = writer.offset;
1396
+ writeUint32(writer, 0); // content length
1397
+ writeSignature(writer, sound ? 'sndM' : 'txtC');
1398
+ writeUint32(writer, 0); // data length
1399
+ const dataOffset = writer.offset;
1400
+
1401
+ if (sound) {
1402
+ writeBytes(writer, annotation.data as Uint8Array);
1403
+ } else {
1404
+ writeUint16(writer, 0xfeff); // unicode string indicator
1405
+ const text = (annotation.data as string).replace(/\n/g, '\r');
1406
+ for (let i = 0; i < text.length; i++) writeUint16(writer, text.charCodeAt(i));
1407
+ }
1408
+
1409
+ writer.view.setUint32(lengthOffset, writer.offset - lengthOffset, false);
1410
+ writer.view.setUint32(contentOffset, writer.offset - contentOffset, false);
1411
+ writer.view.setUint32(dataOffset - 4, writer.offset - dataOffset, false);
1412
+ }
1413
+ }
1414
+ );
1415
+
1416
+ interface FileOpenDescriptor {
1417
+ compInfo: { compID: number; originalCompID: number; };
1418
+ }
1419
+
1420
+ addHandler(
1421
+ 'lnk2',
1422
+ (target: any) => !!(target as Psd).linkedFiles && (target as Psd).linkedFiles!.length > 0,
1423
+ (reader, target, left, _, options) => {
1424
+ const psd = target as Psd;
1425
+ psd.linkedFiles = [];
1426
+
1427
+ while (left() > 8) {
1428
+ let size = readLength64(reader); // size
1429
+ const startOffset = reader.offset;
1430
+ const type = readSignature(reader) as 'liFD' | 'liFE' | 'liFA';
1431
+ const version = readInt32(reader);
1432
+ const id = readPascalString(reader, 1);
1433
+ const name = readUnicodeString(reader);
1434
+ const fileType = readSignature(reader).trim(); // ' ' if empty
1435
+ const fileCreator = readSignature(reader).trim(); // ' ' or '\0\0\0\0' if empty
1436
+ const dataSize = readLength64(reader);
1437
+ const hasFileOpenDescriptor = readUint8(reader);
1438
+ const fileOpenDescriptor = hasFileOpenDescriptor ? readVersionAndDescriptor(reader) as FileOpenDescriptor : undefined;
1439
+ const linkedFileDescriptor = type === 'liFE' ? readVersionAndDescriptor(reader) : undefined;
1440
+ const file: LinkedFile = { id, name, data: undefined };
1441
+
1442
+ if (fileType) file.type = fileType;
1443
+ if (fileCreator) file.creator = fileCreator;
1444
+ if (fileOpenDescriptor) file.descriptor = fileOpenDescriptor;
1445
+
1446
+ if (type === 'liFE' && version > 3) {
1447
+ const year = readInt32(reader);
1448
+ const month = readUint8(reader);
1449
+ const day = readUint8(reader);
1450
+ const hour = readUint8(reader);
1451
+ const minute = readUint8(reader);
1452
+ const seconds = readFloat64(reader);
1453
+ const wholeSeconds = Math.floor(seconds);
1454
+ const ms = (seconds - wholeSeconds) * 1000;
1455
+ file.time = new Date(year, month, day, hour, minute, wholeSeconds, ms);
1456
+ }
1457
+
1458
+ const fileSize = type === 'liFE' ? readLength64(reader) : 0;
1459
+ if (type === 'liFA') skipBytes(reader, 8);
1460
+ if (type === 'liFD') file.data = readBytes(reader, dataSize);
1461
+ if (version >= 5) file.childDocumentID = readUnicodeString(reader);
1462
+ if (version >= 6) file.assetModTime = readFloat64(reader);
1463
+ if (version >= 7) file.assetLockedState = readUint8(reader);
1464
+ if (type === 'liFE') file.data = readBytes(reader, fileSize);
1465
+
1466
+ if (options.skipLinkedFilesData) file.data = undefined;
1467
+
1468
+ psd.linkedFiles.push(file);
1469
+ linkedFileDescriptor;
1470
+
1471
+ while (size % 4) size++;
1472
+ reader.offset = startOffset + size;
1473
+ }
1474
+
1475
+ skipBytes(reader, left()); // ?
1476
+ },
1477
+ (writer, target) => {
1478
+ const psd = target as Psd;
1479
+
1480
+ for (const file of psd.linkedFiles!) {
1481
+ let version = 2;
1482
+
1483
+ if (file.assetLockedState != null) version = 7;
1484
+ else if (file.assetModTime != null) version = 6;
1485
+ else if (file.childDocumentID != null) version = 5;
1486
+ // TODO: else if (file.time != null) version = 3; (only for liFE)
1487
+
1488
+ writeUint32(writer, 0);
1489
+ writeUint32(writer, 0); // size
1490
+ const sizeOffset = writer.offset;
1491
+ writeSignature(writer, file.data ? 'liFD' : 'liFA');
1492
+ writeInt32(writer, version);
1493
+ writePascalString(writer, file.id || '', 1);
1494
+ writeUnicodeStringWithPadding(writer, file.name || '');
1495
+ writeSignature(writer, file.type ? `${file.type} `.substring(0, 4) : ' ');
1496
+ writeSignature(writer, file.creator ? `${file.creator} `.substring(0, 4) : '\0\0\0\0');
1497
+ writeLength64(writer, file.data ? file.data.byteLength : 0);
1498
+
1499
+ if (file.descriptor && file.descriptor.compInfo) {
1500
+ const desc: FileOpenDescriptor = {
1501
+ compInfo: file.descriptor.compInfo,
1502
+ };
1503
+
1504
+ writeUint8(writer, 1);
1505
+ writeVersionAndDescriptor(writer, '', 'null', desc);
1506
+ } else {
1507
+ writeUint8(writer, 0);
1508
+ }
1509
+
1510
+ if (file.data) writeBytes(writer, file.data);
1511
+ else writeLength64(writer, 0);
1512
+ if (version >= 5) writeUnicodeStringWithPadding(writer, file.childDocumentID || '');
1513
+ if (version >= 6) writeFloat64(writer, file.assetModTime || 0);
1514
+ if (version >= 7) writeUint8(writer, file.assetLockedState || 0);
1515
+
1516
+ let size = writer.offset - sizeOffset;
1517
+ writer.view.setUint32(sizeOffset - 4, size, false); // write size
1518
+
1519
+ while (size % 4) {
1520
+ size++;
1521
+ writeUint8(writer, 0);
1522
+ }
1523
+ }
1524
+ },
1525
+ );
1526
+ addHandlerAlias('lnkD', 'lnk2');
1527
+ addHandlerAlias('lnk3', 'lnk2');
1528
+
1529
+ // this seems to just be zero size block, ignore it
1530
+ addHandler(
1531
+ 'lnkE',
1532
+ target => (target as any)._lnkE !== undefined,
1533
+ (reader, target, left, _psds, options) => {
1534
+ if (options.logMissingFeatures && left()) {
1535
+ console.log(`Non-empty lnkE layer info (${left()} bytes)`);
1536
+ }
1537
+
1538
+ if (MOCK_HANDLERS) {
1539
+ (target as any)._lnkE = readBytes(reader, left());
1540
+ }
1541
+ },
1542
+ (writer, target) => MOCK_HANDLERS && writeBytes(writer, (target as any)._lnkE),
1543
+ );
1544
+
1545
+ interface ExtensionDesc {
1546
+ generatorSettings: {
1547
+ generator_45_assets: { json: string; };
1548
+ layerTime: number;
1549
+ };
1550
+ }
1551
+
1552
+ addHandler(
1553
+ 'pths',
1554
+ hasKey('pathList'),
1555
+ (reader, target) => {
1556
+ const descriptor = readVersionAndDescriptor(reader);
1557
+
1558
+ target.pathList = []; // TODO: read paths (find example with non-empty list)
1559
+
1560
+ descriptor;
1561
+ // console.log('pths', descriptor); // TODO: remove this
1562
+ },
1563
+ (writer, _target) => {
1564
+ const descriptor = {
1565
+ pathList: [], // TODO: write paths
1566
+ };
1567
+
1568
+ writeVersionAndDescriptor(writer, '', 'pathsDataClass', descriptor);
1569
+ },
1570
+ );
1571
+
1572
+ addHandler(
1573
+ 'lyvr',
1574
+ hasKey('version'),
1575
+ (reader, target) => target.version = readUint32(reader),
1576
+ (writer, target) => writeUint32(writer, target.version!),
1577
+ );
1578
+
1579
+ function adjustmentType(type: string) {
1580
+ return (target: LayerAdditionalInfo) => !!target.adjustment && target.adjustment.type === type;
1581
+ }
1582
+
1583
+ addHandler(
1584
+ 'brit',
1585
+ adjustmentType('brightness/contrast'),
1586
+ (reader, target, left) => {
1587
+ if (!target.adjustment) { // ignore if got one from CgEd block
1588
+ target.adjustment = {
1589
+ type: 'brightness/contrast',
1590
+ brightness: readInt16(reader),
1591
+ contrast: readInt16(reader),
1592
+ meanValue: readInt16(reader),
1593
+ labColorOnly: !!readUint8(reader),
1594
+ useLegacy: true,
1595
+ };
1596
+ }
1597
+
1598
+ skipBytes(reader, left());
1599
+ },
1600
+ (writer, target) => {
1601
+ const info = target.adjustment as BrightnessAdjustment;
1602
+ writeInt16(writer, info.brightness || 0);
1603
+ writeInt16(writer, info.contrast || 0);
1604
+ writeInt16(writer, info.meanValue ?? 127);
1605
+ writeUint8(writer, info.labColorOnly ? 1 : 0);
1606
+ writeZeros(writer, 1);
1607
+ },
1608
+ );
1609
+
1610
+ function readLevelsChannel(reader: PsdReader): LevelsAdjustmentChannel {
1611
+ const shadowInput = readInt16(reader);
1612
+ const highlightInput = readInt16(reader);
1613
+ const shadowOutput = readInt16(reader);
1614
+ const highlightOutput = readInt16(reader);
1615
+ const midtoneInput = readInt16(reader) / 100;
1616
+ return { shadowInput, highlightInput, shadowOutput, highlightOutput, midtoneInput };
1617
+ }
1618
+
1619
+ function writeLevelsChannel(writer: PsdWriter, channel: LevelsAdjustmentChannel) {
1620
+ writeInt16(writer, channel.shadowInput);
1621
+ writeInt16(writer, channel.highlightInput);
1622
+ writeInt16(writer, channel.shadowOutput);
1623
+ writeInt16(writer, channel.highlightOutput);
1624
+ writeInt16(writer, Math.round(channel.midtoneInput * 100));
1625
+ }
1626
+
1627
+ addHandler(
1628
+ 'levl',
1629
+ adjustmentType('levels'),
1630
+ (reader, target, left) => {
1631
+ if (readUint16(reader) !== 2) throw new Error('Invalid levl version');
1632
+
1633
+ target.adjustment = {
1634
+ ...target.adjustment as PresetInfo,
1635
+ type: 'levels',
1636
+ rgb: readLevelsChannel(reader),
1637
+ red: readLevelsChannel(reader),
1638
+ green: readLevelsChannel(reader),
1639
+ blue: readLevelsChannel(reader),
1640
+ };
1641
+
1642
+ skipBytes(reader, left());
1643
+ },
1644
+ (writer, target) => {
1645
+ const info = target.adjustment as LevelsAdjustment;
1646
+ const defaultChannel = {
1647
+ shadowInput: 0,
1648
+ highlightInput: 255,
1649
+ shadowOutput: 0,
1650
+ highlightOutput: 255,
1651
+ midtoneInput: 1,
1652
+ };
1653
+
1654
+ writeUint16(writer, 2); // version
1655
+ writeLevelsChannel(writer, info.rgb || defaultChannel);
1656
+ writeLevelsChannel(writer, info.red || defaultChannel);
1657
+ writeLevelsChannel(writer, info.blue || defaultChannel);
1658
+ writeLevelsChannel(writer, info.green || defaultChannel);
1659
+ for (let i = 0; i < 59; i++) writeLevelsChannel(writer, defaultChannel);
1660
+ },
1661
+ );
1662
+
1663
+ function readCurveChannel(reader: PsdReader) {
1664
+ const nodes = readUint16(reader);
1665
+ const channel: CurvesAdjustmentChannel = [];
1666
+
1667
+ for (let j = 0; j < nodes; j++) {
1668
+ const output = readInt16(reader);
1669
+ const input = readInt16(reader);
1670
+ channel.push({ input, output });
1671
+ }
1672
+
1673
+ return channel;
1674
+ }
1675
+
1676
+ function writeCurveChannel(writer: PsdWriter, channel: CurvesAdjustmentChannel) {
1677
+ writeUint16(writer, channel.length);
1678
+
1679
+ for (const n of channel) {
1680
+ writeUint16(writer, n.output);
1681
+ writeUint16(writer, n.input);
1682
+ }
1683
+ }
1684
+
1685
+ addHandler(
1686
+ 'curv',
1687
+ adjustmentType('curves'),
1688
+ (reader, target, left) => {
1689
+ readUint8(reader);
1690
+ if (readUint16(reader) !== 1) throw new Error('Invalid curv version');
1691
+ readUint16(reader);
1692
+ const channels = readUint16(reader);
1693
+ const info: CurvesAdjustment = { type: 'curves' };
1694
+
1695
+ if (channels & 1) info.rgb = readCurveChannel(reader);
1696
+ if (channels & 2) info.red = readCurveChannel(reader);
1697
+ if (channels & 4) info.green = readCurveChannel(reader);
1698
+ if (channels & 8) info.blue = readCurveChannel(reader);
1699
+
1700
+ target.adjustment = {
1701
+ ...target.adjustment as PresetInfo,
1702
+ ...info,
1703
+ };
1704
+
1705
+ // ignoring, duplicate information
1706
+ // checkSignature(reader, 'Crv ');
1707
+
1708
+ // const cVersion = readUint16(reader);
1709
+ // readUint16(reader);
1710
+ // const channelCount = readUint16(reader);
1711
+
1712
+ // for (let i = 0; i < channelCount; i++) {
1713
+ // const index = readUint16(reader);
1714
+ // const nodes = readUint16(reader);
1715
+
1716
+ // for (let j = 0; j < nodes; j++) {
1717
+ // const output = readInt16(reader);
1718
+ // const input = readInt16(reader);
1719
+ // }
1720
+ // }
1721
+
1722
+ skipBytes(reader, left());
1723
+ },
1724
+ (writer, target) => {
1725
+ const info = target.adjustment as CurvesAdjustment;
1726
+ const { rgb, red, green, blue } = info;
1727
+ let channels = 0;
1728
+ let channelCount = 0;
1729
+
1730
+ if (rgb && rgb.length) { channels |= 1; channelCount++; }
1731
+ if (red && red.length) { channels |= 2; channelCount++; }
1732
+ if (green && green.length) { channels |= 4; channelCount++; }
1733
+ if (blue && blue.length) { channels |= 8; channelCount++; }
1734
+
1735
+ writeUint8(writer, 0);
1736
+ writeUint16(writer, 1); // version
1737
+ writeUint16(writer, 0);
1738
+ writeUint16(writer, channels);
1739
+
1740
+ if (rgb && rgb.length) writeCurveChannel(writer, rgb);
1741
+ if (red && red.length) writeCurveChannel(writer, red);
1742
+ if (green && green.length) writeCurveChannel(writer, green);
1743
+ if (blue && blue.length) writeCurveChannel(writer, blue);
1744
+
1745
+ writeSignature(writer, 'Crv ');
1746
+ writeUint16(writer, 4); // version
1747
+ writeUint16(writer, 0);
1748
+ writeUint16(writer, channelCount);
1749
+
1750
+ if (rgb && rgb.length) { writeUint16(writer, 0); writeCurveChannel(writer, rgb); }
1751
+ if (red && red.length) { writeUint16(writer, 1); writeCurveChannel(writer, red); }
1752
+ if (green && green.length) { writeUint16(writer, 2); writeCurveChannel(writer, green); }
1753
+ if (blue && blue.length) { writeUint16(writer, 3); writeCurveChannel(writer, blue); }
1754
+
1755
+ writeZeros(writer, 2);
1756
+ },
1757
+ );
1758
+
1759
+ addHandler(
1760
+ 'expA',
1761
+ adjustmentType('exposure'),
1762
+ (reader, target, left) => {
1763
+ if (readUint16(reader) !== 1) throw new Error('Invalid expA version');
1764
+
1765
+ target.adjustment = {
1766
+ ...target.adjustment as PresetInfo,
1767
+ type: 'exposure',
1768
+ exposure: readFloat32(reader),
1769
+ offset: readFloat32(reader),
1770
+ gamma: readFloat32(reader),
1771
+ };
1772
+
1773
+ skipBytes(reader, left());
1774
+ },
1775
+ (writer, target) => {
1776
+ const info = target.adjustment as ExposureAdjustment;
1777
+ writeUint16(writer, 1); // version
1778
+ writeFloat32(writer, info.exposure!);
1779
+ writeFloat32(writer, info.offset!);
1780
+ writeFloat32(writer, info.gamma!);
1781
+ writeZeros(writer, 2);
1782
+ },
1783
+ );
1784
+
1785
+ interface VibranceDescriptor {
1786
+ vibrance?: number;
1787
+ Strt?: number;
1788
+ }
1789
+
1790
+ addHandler(
1791
+ 'vibA',
1792
+ adjustmentType('vibrance'),
1793
+ (reader, target, left) => {
1794
+ const desc: VibranceDescriptor = readVersionAndDescriptor(reader);
1795
+ target.adjustment = { type: 'vibrance' };
1796
+ if (desc.vibrance !== undefined) target.adjustment.vibrance = desc.vibrance;
1797
+ if (desc.Strt !== undefined) target.adjustment.saturation = desc.Strt;
1798
+
1799
+ skipBytes(reader, left());
1800
+ },
1801
+ (writer, target) => {
1802
+ const info = target.adjustment as VibranceAdjustment;
1803
+ const desc: VibranceDescriptor = {};
1804
+ if (info.vibrance !== undefined) desc.vibrance = info.vibrance;
1805
+ if (info.saturation !== undefined) desc.Strt = info.saturation;
1806
+
1807
+ writeVersionAndDescriptor(writer, '', 'null', desc);
1808
+ },
1809
+ );
1810
+
1811
+ function readHueChannel(reader: PsdReader): HueSaturationAdjustmentChannel {
1812
+ return {
1813
+ a: readInt16(reader),
1814
+ b: readInt16(reader),
1815
+ c: readInt16(reader),
1816
+ d: readInt16(reader),
1817
+ hue: readInt16(reader),
1818
+ saturation: readInt16(reader),
1819
+ lightness: readInt16(reader),
1820
+ };
1821
+ }
1822
+
1823
+ function writeHueChannel(writer: PsdWriter, channel: HueSaturationAdjustmentChannel | undefined) {
1824
+ const c = channel || {} as Partial<HueSaturationAdjustmentChannel>;
1825
+ writeInt16(writer, c.a || 0);
1826
+ writeInt16(writer, c.b || 0);
1827
+ writeInt16(writer, c.c || 0);
1828
+ writeInt16(writer, c.d || 0);
1829
+ writeInt16(writer, c.hue || 0);
1830
+ writeInt16(writer, c.saturation || 0);
1831
+ writeInt16(writer, c.lightness || 0);
1832
+ }
1833
+
1834
+ addHandler(
1835
+ 'hue2',
1836
+ adjustmentType('hue/saturation'),
1837
+ (reader, target, left) => {
1838
+ if (readUint16(reader) !== 2) throw new Error('Invalid hue2 version');
1839
+
1840
+ target.adjustment = {
1841
+ ...target.adjustment as PresetInfo,
1842
+ type: 'hue/saturation',
1843
+ master: readHueChannel(reader),
1844
+ reds: readHueChannel(reader),
1845
+ yellows: readHueChannel(reader),
1846
+ greens: readHueChannel(reader),
1847
+ cyans: readHueChannel(reader),
1848
+ blues: readHueChannel(reader),
1849
+ magentas: readHueChannel(reader),
1850
+ };
1851
+
1852
+ skipBytes(reader, left());
1853
+ },
1854
+ (writer, target) => {
1855
+ const info = target.adjustment as HueSaturationAdjustment;
1856
+
1857
+ writeUint16(writer, 2); // version
1858
+ writeHueChannel(writer, info.master);
1859
+ writeHueChannel(writer, info.reds);
1860
+ writeHueChannel(writer, info.yellows);
1861
+ writeHueChannel(writer, info.greens);
1862
+ writeHueChannel(writer, info.cyans);
1863
+ writeHueChannel(writer, info.blues);
1864
+ writeHueChannel(writer, info.magentas);
1865
+ },
1866
+ );
1867
+
1868
+ function readColorBalance(reader: PsdReader): ColorBalanceValues {
1869
+ return {
1870
+ cyanRed: readInt16(reader),
1871
+ magentaGreen: readInt16(reader),
1872
+ yellowBlue: readInt16(reader),
1873
+ };
1874
+ }
1875
+
1876
+ function writeColorBalance(writer: PsdWriter, value: Partial<ColorBalanceValues>) {
1877
+ writeInt16(writer, value.cyanRed || 0);
1878
+ writeInt16(writer, value.magentaGreen || 0);
1879
+ writeInt16(writer, value.yellowBlue || 0);
1880
+ }
1881
+
1882
+ addHandler(
1883
+ 'blnc',
1884
+ adjustmentType('color balance'),
1885
+ (reader, target, left) => {
1886
+ target.adjustment = {
1887
+ type: 'color balance',
1888
+ shadows: readColorBalance(reader),
1889
+ midtones: readColorBalance(reader),
1890
+ highlights: readColorBalance(reader),
1891
+ preserveLuminosity: !!readUint8(reader),
1892
+ };
1893
+
1894
+ skipBytes(reader, left());
1895
+ },
1896
+ (writer, target) => {
1897
+ const info = target.adjustment as ColorBalanceAdjustment;
1898
+ writeColorBalance(writer, info.shadows || {});
1899
+ writeColorBalance(writer, info.midtones || {});
1900
+ writeColorBalance(writer, info.highlights || {});
1901
+ writeUint8(writer, info.preserveLuminosity ? 1 : 0);
1902
+ writeZeros(writer, 1);
1903
+ },
1904
+ );
1905
+
1906
+ interface BlackAndWhiteDescriptor {
1907
+ 'Rd ': number;
1908
+ Yllw: number;
1909
+ 'Grn ': number;
1910
+ 'Cyn ': number;
1911
+ 'Bl ': number;
1912
+ Mgnt: number;
1913
+ useTint: boolean;
1914
+ tintColor?: DescriptorColor;
1915
+ bwPresetKind: number;
1916
+ blackAndWhitePresetFileName: string;
1917
+ }
1918
+
1919
+ addHandler(
1920
+ 'blwh',
1921
+ adjustmentType('black & white'),
1922
+ (reader, target, left) => {
1923
+ const desc: BlackAndWhiteDescriptor = readVersionAndDescriptor(reader);
1924
+ target.adjustment = {
1925
+ type: 'black & white',
1926
+ reds: desc['Rd '],
1927
+ yellows: desc.Yllw,
1928
+ greens: desc['Grn '],
1929
+ cyans: desc['Cyn '],
1930
+ blues: desc['Bl '],
1931
+ magentas: desc.Mgnt,
1932
+ useTint: !!desc.useTint,
1933
+ presetKind: desc.bwPresetKind,
1934
+ presetFileName: desc.blackAndWhitePresetFileName,
1935
+ };
1936
+
1937
+ if (desc.tintColor !== undefined) target.adjustment.tintColor = parseColor(desc.tintColor);
1938
+
1939
+ skipBytes(reader, left());
1940
+ },
1941
+ (writer, target) => {
1942
+ const info = target.adjustment as BlackAndWhiteAdjustment;
1943
+ const desc: BlackAndWhiteDescriptor = {
1944
+ 'Rd ': info.reds || 0,
1945
+ Yllw: info.yellows || 0,
1946
+ 'Grn ': info.greens || 0,
1947
+ 'Cyn ': info.cyans || 0,
1948
+ 'Bl ': info.blues || 0,
1949
+ Mgnt: info.magentas || 0,
1950
+ useTint: !!info.useTint,
1951
+ tintColor: serializeColor(info.tintColor),
1952
+ bwPresetKind: info.presetKind || 0,
1953
+ blackAndWhitePresetFileName: info.presetFileName || '',
1954
+ };
1955
+
1956
+ writeVersionAndDescriptor(writer, '', 'null', desc);
1957
+ },
1958
+ );
1959
+
1960
+ addHandler(
1961
+ 'phfl',
1962
+ adjustmentType('photo filter'),
1963
+ (reader, target, left) => {
1964
+ const version = readUint16(reader);
1965
+ if (version !== 2 && version !== 3) throw new Error('Invalid phfl version');
1966
+
1967
+ let color: Color;
1968
+
1969
+ if (version === 2) {
1970
+ color = readColor(reader);
1971
+ } else { // version 3
1972
+ // TODO: test this, this is probably wrong
1973
+ color = {
1974
+ l: readInt32(reader) / 100,
1975
+ a: readInt32(reader) / 100,
1976
+ b: readInt32(reader) / 100,
1977
+ };
1978
+ }
1979
+
1980
+ target.adjustment = {
1981
+ type: 'photo filter',
1982
+ color,
1983
+ density: readUint32(reader) / 100,
1984
+ preserveLuminosity: !!readUint8(reader),
1985
+ };
1986
+
1987
+ skipBytes(reader, left());
1988
+ },
1989
+ (writer, target) => {
1990
+ const info = target.adjustment as PhotoFilterAdjustment;
1991
+ writeUint16(writer, 2); // version
1992
+ writeColor(writer, info.color || { l: 0, a: 0, b: 0 });
1993
+ writeUint32(writer, (info.density || 0) * 100);
1994
+ writeUint8(writer, info.preserveLuminosity ? 1 : 0);
1995
+ writeZeros(writer, 3);
1996
+ },
1997
+ );
1998
+
1999
+ function readMixrChannel(reader: PsdReader): ChannelMixerChannel {
2000
+ const red = readInt16(reader);
2001
+ const green = readInt16(reader);
2002
+ const blue = readInt16(reader);
2003
+ skipBytes(reader, 2);
2004
+ const constant = readInt16(reader);
2005
+ return { red, green, blue, constant };
2006
+ }
2007
+
2008
+ function writeMixrChannel(writer: PsdWriter, channel: ChannelMixerChannel | undefined) {
2009
+ const c = channel || {} as Partial<ChannelMixerChannel>;
2010
+ writeInt16(writer, c.red!);
2011
+ writeInt16(writer, c.green!);
2012
+ writeInt16(writer, c.blue!);
2013
+ writeZeros(writer, 2);
2014
+ writeInt16(writer, c.constant!);
2015
+ }
2016
+
2017
+ addHandler(
2018
+ 'mixr',
2019
+ adjustmentType('channel mixer'),
2020
+ (reader, target, left) => {
2021
+ if (readUint16(reader) !== 1) throw new Error('Invalid mixr version');
2022
+
2023
+ const adjustment: ChannelMixerAdjustment = target.adjustment = {
2024
+ ...target.adjustment as PresetInfo,
2025
+ type: 'channel mixer',
2026
+ monochrome: !!readUint16(reader),
2027
+ };
2028
+
2029
+ if (!adjustment.monochrome) {
2030
+ adjustment.red = readMixrChannel(reader);
2031
+ adjustment.green = readMixrChannel(reader);
2032
+ adjustment.blue = readMixrChannel(reader);
2033
+ }
2034
+
2035
+ adjustment.gray = readMixrChannel(reader);
2036
+
2037
+ skipBytes(reader, left());
2038
+ },
2039
+ (writer, target) => {
2040
+ const info = target.adjustment as ChannelMixerAdjustment;
2041
+ writeUint16(writer, 1); // version
2042
+ writeUint16(writer, info.monochrome ? 1 : 0);
2043
+
2044
+ if (info.monochrome) {
2045
+ writeMixrChannel(writer, info.gray);
2046
+ writeZeros(writer, 3 * 5 * 2);
2047
+ } else {
2048
+ writeMixrChannel(writer, info.red);
2049
+ writeMixrChannel(writer, info.green);
2050
+ writeMixrChannel(writer, info.blue);
2051
+ writeMixrChannel(writer, info.gray);
2052
+ }
2053
+ },
2054
+ );
2055
+
2056
+ const colorLookupType = createEnum<'3dlut' | 'abstractProfile' | 'deviceLinkProfile'>('colorLookupType', '3DLUT', {
2057
+ '3dlut': '3DLUT',
2058
+ abstractProfile: 'abstractProfile',
2059
+ deviceLinkProfile: 'deviceLinkProfile',
2060
+ });
2061
+
2062
+ const LUTFormatType = createEnum<'look' | 'cube' | '3dl'>('LUTFormatType', 'look', {
2063
+ look: 'LUTFormatLOOK',
2064
+ cube: 'LUTFormatCUBE',
2065
+ '3dl': 'LUTFormat3DL',
2066
+ });
2067
+
2068
+ const colorLookupOrder = createEnum<'rgb' | 'bgr'>('colorLookupOrder', 'rgb', {
2069
+ rgb: 'rgbOrder',
2070
+ bgr: 'bgrOrder',
2071
+ });
2072
+
2073
+ interface ColorLookupDescriptor {
2074
+ lookupType?: string;
2075
+ 'Nm '?: string;
2076
+ Dthr?: boolean;
2077
+ profile?: Uint8Array;
2078
+ LUTFormat?: string;
2079
+ dataOrder?: string;
2080
+ tableOrder?: string;
2081
+ LUT3DFileData?: Uint8Array;
2082
+ LUT3DFileName?: string;
2083
+ }
2084
+
2085
+ addHandler(
2086
+ 'clrL',
2087
+ adjustmentType('color lookup'),
2088
+ (reader, target, left) => {
2089
+ if (readUint16(reader) !== 1) throw new Error('Invalid clrL version');
2090
+
2091
+ const desc: ColorLookupDescriptor = readVersionAndDescriptor(reader);
2092
+ target.adjustment = { type: 'color lookup' };
2093
+ const info = target.adjustment;
2094
+
2095
+ if (desc.lookupType !== undefined) info.lookupType = colorLookupType.decode(desc.lookupType);
2096
+ if (desc['Nm '] !== undefined) info.name = desc['Nm '];
2097
+ if (desc.Dthr !== undefined) info.dither = desc.Dthr;
2098
+ if (desc.profile !== undefined) info.profile = desc.profile;
2099
+ if (desc.LUTFormat !== undefined) info.lutFormat = LUTFormatType.decode(desc.LUTFormat);
2100
+ if (desc.dataOrder !== undefined) info.dataOrder = colorLookupOrder.decode(desc.dataOrder);
2101
+ if (desc.tableOrder !== undefined) info.tableOrder = colorLookupOrder.decode(desc.tableOrder);
2102
+ if (desc.LUT3DFileData !== undefined) info.lut3DFileData = desc.LUT3DFileData;
2103
+ if (desc.LUT3DFileName !== undefined) info.lut3DFileName = desc.LUT3DFileName;
2104
+
2105
+ skipBytes(reader, left());
2106
+ },
2107
+ (writer, target) => {
2108
+ const info = target.adjustment as ColorLookupAdjustment;
2109
+ const desc: ColorLookupDescriptor = {};
2110
+
2111
+ if (info.lookupType !== undefined) desc.lookupType = colorLookupType.encode(info.lookupType);
2112
+ if (info.name !== undefined) desc['Nm '] = info.name;
2113
+ if (info.dither !== undefined) desc.Dthr = info.dither;
2114
+ if (info.profile !== undefined) desc.profile = info.profile;
2115
+ if (info.lutFormat !== undefined) desc.LUTFormat = LUTFormatType.encode(info.lutFormat);
2116
+ if (info.dataOrder !== undefined) desc.dataOrder = colorLookupOrder.encode(info.dataOrder);
2117
+ if (info.tableOrder !== undefined) desc.tableOrder = colorLookupOrder.encode(info.tableOrder);
2118
+ if (info.lut3DFileData !== undefined) desc.LUT3DFileData = info.lut3DFileData;
2119
+ if (info.lut3DFileName !== undefined) desc.LUT3DFileName = info.lut3DFileName;
2120
+
2121
+ writeUint16(writer, 1); // version
2122
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2123
+ },
2124
+ );
2125
+
2126
+ addHandler(
2127
+ 'nvrt',
2128
+ adjustmentType('invert'),
2129
+ (reader, target, left) => {
2130
+ target.adjustment = { type: 'invert' };
2131
+ skipBytes(reader, left());
2132
+ },
2133
+ () => {
2134
+ // nothing to write here
2135
+ },
2136
+ );
2137
+
2138
+ addHandler(
2139
+ 'post',
2140
+ adjustmentType('posterize'),
2141
+ (reader, target, left) => {
2142
+ target.adjustment = {
2143
+ type: 'posterize',
2144
+ levels: readUint16(reader),
2145
+ };
2146
+ skipBytes(reader, left());
2147
+ },
2148
+ (writer, target) => {
2149
+ const info = target.adjustment as PosterizeAdjustment;
2150
+ writeUint16(writer, info.levels ?? 4);
2151
+ writeZeros(writer, 2);
2152
+ },
2153
+ );
2154
+
2155
+ addHandler(
2156
+ 'thrs',
2157
+ adjustmentType('threshold'),
2158
+ (reader, target, left) => {
2159
+ target.adjustment = {
2160
+ type: 'threshold',
2161
+ level: readUint16(reader),
2162
+ };
2163
+ skipBytes(reader, left());
2164
+ },
2165
+ (writer, target) => {
2166
+ const info = target.adjustment as ThresholdAdjustment;
2167
+ writeUint16(writer, info.level ?? 128);
2168
+ writeZeros(writer, 2);
2169
+ },
2170
+ );
2171
+
2172
+ const grdmColorModels = ['', '', '', 'rgb', 'hsb', '', 'lab'];
2173
+
2174
+ addHandler(
2175
+ 'grdm',
2176
+ adjustmentType('gradient map'),
2177
+ (reader, target, left) => {
2178
+ if (readUint16(reader) !== 1) throw new Error('Invalid grdm version');
2179
+
2180
+ const info: GradientMapAdjustment = {
2181
+ type: 'gradient map',
2182
+ gradientType: 'solid',
2183
+ };
2184
+
2185
+ info.reverse = !!readUint8(reader);
2186
+ info.dither = !!readUint8(reader);
2187
+ info.name = readUnicodeString(reader);
2188
+ info.colorStops = [];
2189
+ info.opacityStops = [];
2190
+
2191
+ const stopsCount = readUint16(reader);
2192
+
2193
+ for (let i = 0; i < stopsCount; i++) {
2194
+ info.colorStops.push({
2195
+ location: readUint32(reader),
2196
+ midpoint: readUint32(reader) / 100,
2197
+ color: readColor(reader),
2198
+ });
2199
+ skipBytes(reader, 2);
2200
+ }
2201
+
2202
+ const opacityStopsCount = readUint16(reader);
2203
+
2204
+ for (let i = 0; i < opacityStopsCount; i++) {
2205
+ info.opacityStops.push({
2206
+ location: readUint32(reader),
2207
+ midpoint: readUint32(reader) / 100,
2208
+ opacity: readUint16(reader) / 0xff,
2209
+ });
2210
+ }
2211
+
2212
+ const expansionCount = readUint16(reader);
2213
+ if (expansionCount !== 2) throw new Error('Invalid grdm expansion count');
2214
+
2215
+ const interpolation = readUint16(reader);
2216
+ info.smoothness = interpolation / 4096;
2217
+
2218
+ const length = readUint16(reader);
2219
+ if (length !== 32) throw new Error('Invalid grdm length');
2220
+
2221
+ info.gradientType = readUint16(reader) ? 'noise' : 'solid';
2222
+ info.randomSeed = readUint32(reader);
2223
+ info.addTransparency = !!readUint16(reader);
2224
+ info.restrictColors = !!readUint16(reader);
2225
+ info.roughness = readUint32(reader) / 4096;
2226
+ info.colorModel = (grdmColorModels[readUint16(reader)] || 'rgb') as 'rgb' | 'hsb' | 'lab';
2227
+
2228
+ info.min = [
2229
+ readUint16(reader) / 0x8000,
2230
+ readUint16(reader) / 0x8000,
2231
+ readUint16(reader) / 0x8000,
2232
+ readUint16(reader) / 0x8000,
2233
+ ];
2234
+
2235
+ info.max = [
2236
+ readUint16(reader) / 0x8000,
2237
+ readUint16(reader) / 0x8000,
2238
+ readUint16(reader) / 0x8000,
2239
+ readUint16(reader) / 0x8000,
2240
+ ];
2241
+
2242
+ skipBytes(reader, left());
2243
+
2244
+ for (const s of info.colorStops) s.location /= interpolation;
2245
+ for (const s of info.opacityStops) s.location /= interpolation;
2246
+
2247
+ target.adjustment = info;
2248
+ },
2249
+ (writer, target) => {
2250
+ const info = target.adjustment as GradientMapAdjustment;
2251
+
2252
+ writeUint16(writer, 1); // version
2253
+ writeUint8(writer, info.reverse ? 1 : 0);
2254
+ writeUint8(writer, info.dither ? 1 : 0);
2255
+ writeUnicodeStringWithPadding(writer, info.name || '');
2256
+ writeUint16(writer, info.colorStops && info.colorStops.length || 0);
2257
+
2258
+ const interpolation = Math.round((info.smoothness ?? 1) * 4096);
2259
+
2260
+ for (const s of info.colorStops || []) {
2261
+ writeUint32(writer, Math.round(s.location * interpolation));
2262
+ writeUint32(writer, Math.round(s.midpoint * 100));
2263
+ writeColor(writer, s.color);
2264
+ writeZeros(writer, 2);
2265
+ }
2266
+
2267
+ writeUint16(writer, info.opacityStops && info.opacityStops.length || 0);
2268
+
2269
+ for (const s of info.opacityStops || []) {
2270
+ writeUint32(writer, Math.round(s.location * interpolation));
2271
+ writeUint32(writer, Math.round(s.midpoint * 100));
2272
+ writeUint16(writer, Math.round(s.opacity * 0xff));
2273
+ }
2274
+
2275
+ writeUint16(writer, 2); // expansion count
2276
+ writeUint16(writer, interpolation);
2277
+ writeUint16(writer, 32); // length
2278
+ writeUint16(writer, info.gradientType === 'noise' ? 1 : 0);
2279
+ writeUint32(writer, info.randomSeed || 0);
2280
+ writeUint16(writer, info.addTransparency ? 1 : 0);
2281
+ writeUint16(writer, info.restrictColors ? 1 : 0);
2282
+ writeUint32(writer, Math.round((info.roughness ?? 1) * 4096));
2283
+ const colorModel = grdmColorModels.indexOf(info.colorModel ?? 'rgb');
2284
+ writeUint16(writer, colorModel === -1 ? 3 : colorModel);
2285
+
2286
+ for (let i = 0; i < 4; i++)
2287
+ writeUint16(writer, Math.round((info.min && info.min[i] || 0) * 0x8000));
2288
+
2289
+ for (let i = 0; i < 4; i++)
2290
+ writeUint16(writer, Math.round((info.max && info.max[i] || 0) * 0x8000));
2291
+
2292
+ writeZeros(writer, 4);
2293
+ },
2294
+ );
2295
+
2296
+ function readSelectiveColors(reader: PsdReader): CMYK {
2297
+ return {
2298
+ c: readInt16(reader),
2299
+ m: readInt16(reader),
2300
+ y: readInt16(reader),
2301
+ k: readInt16(reader),
2302
+ };
2303
+ }
2304
+
2305
+ function writeSelectiveColors(writer: PsdWriter, cmyk: CMYK | undefined) {
2306
+ const c = cmyk || {} as Partial<CMYK>;
2307
+ writeInt16(writer, c.c!);
2308
+ writeInt16(writer, c.m!);
2309
+ writeInt16(writer, c.y!);
2310
+ writeInt16(writer, c.k!);
2311
+ }
2312
+
2313
+ addHandler(
2314
+ 'selc',
2315
+ adjustmentType('selective color'),
2316
+ (reader, target) => {
2317
+ if (readUint16(reader) !== 1) throw new Error('Invalid selc version');
2318
+
2319
+ const mode = readUint16(reader) ? 'absolute' : 'relative';
2320
+ skipBytes(reader, 8);
2321
+
2322
+ target.adjustment = {
2323
+ type: 'selective color',
2324
+ mode,
2325
+ reds: readSelectiveColors(reader),
2326
+ yellows: readSelectiveColors(reader),
2327
+ greens: readSelectiveColors(reader),
2328
+ cyans: readSelectiveColors(reader),
2329
+ blues: readSelectiveColors(reader),
2330
+ magentas: readSelectiveColors(reader),
2331
+ whites: readSelectiveColors(reader),
2332
+ neutrals: readSelectiveColors(reader),
2333
+ blacks: readSelectiveColors(reader),
2334
+ };
2335
+ },
2336
+ (writer, target) => {
2337
+ const info = target.adjustment as SelectiveColorAdjustment;
2338
+
2339
+ writeUint16(writer, 1); // version
2340
+ writeUint16(writer, info.mode === 'absolute' ? 1 : 0);
2341
+ writeZeros(writer, 8);
2342
+ writeSelectiveColors(writer, info.reds);
2343
+ writeSelectiveColors(writer, info.yellows);
2344
+ writeSelectiveColors(writer, info.greens);
2345
+ writeSelectiveColors(writer, info.cyans);
2346
+ writeSelectiveColors(writer, info.blues);
2347
+ writeSelectiveColors(writer, info.magentas);
2348
+ writeSelectiveColors(writer, info.whites);
2349
+ writeSelectiveColors(writer, info.neutrals);
2350
+ writeSelectiveColors(writer, info.blacks);
2351
+ },
2352
+ );
2353
+
2354
+ interface BrightnessContrastDescriptor {
2355
+ Vrsn: number;
2356
+ Brgh: number;
2357
+ Cntr: number;
2358
+ means: number;
2359
+ 'Lab ': boolean;
2360
+ useLegacy: boolean;
2361
+ Auto: boolean;
2362
+ }
2363
+
2364
+ interface PresetDescriptor {
2365
+ Vrsn: number;
2366
+ presetKind: number;
2367
+ presetFileName: string;
2368
+ }
2369
+
2370
+ interface CurvesPresetDescriptor {
2371
+ Vrsn: number;
2372
+ curvesPresetKind: number;
2373
+ curvesPresetFileName: string;
2374
+ }
2375
+
2376
+ interface MixerPresetDescriptor {
2377
+ Vrsn: number;
2378
+ mixerPresetKind: number;
2379
+ mixerPresetFileName: string;
2380
+ }
2381
+
2382
+ addHandler(
2383
+ 'CgEd',
2384
+ target => {
2385
+ const a = target.adjustment;
2386
+
2387
+ if (!a) return false;
2388
+
2389
+ return (a.type === 'brightness/contrast' && !a.useLegacy) ||
2390
+ ((a.type === 'levels' || a.type === 'curves' || a.type === 'exposure' || a.type === 'channel mixer' ||
2391
+ a.type === 'hue/saturation') && a.presetFileName !== undefined);
2392
+ },
2393
+ (reader, target, left) => {
2394
+ const desc = readVersionAndDescriptor(reader) as
2395
+ BrightnessContrastDescriptor | PresetDescriptor | CurvesPresetDescriptor | MixerPresetDescriptor;
2396
+ if (desc.Vrsn !== 1) throw new Error('Invalid CgEd version');
2397
+
2398
+ // this section can specify preset file name for other adjustment types
2399
+ if ('presetFileName' in desc) {
2400
+ target.adjustment = {
2401
+ ...target.adjustment as LevelsAdjustment | ExposureAdjustment | HueSaturationAdjustment,
2402
+ presetKind: desc.presetKind,
2403
+ presetFileName: desc.presetFileName,
2404
+ };
2405
+ } else if ('curvesPresetFileName' in desc) {
2406
+ target.adjustment = {
2407
+ ...target.adjustment as CurvesAdjustment,
2408
+ presetKind: desc.curvesPresetKind,
2409
+ presetFileName: desc.curvesPresetFileName,
2410
+ };
2411
+ } else if ('mixerPresetFileName' in desc) {
2412
+ target.adjustment = {
2413
+ ...target.adjustment as CurvesAdjustment,
2414
+ presetKind: desc.mixerPresetKind,
2415
+ presetFileName: desc.mixerPresetFileName,
2416
+ };
2417
+ } else {
2418
+ target.adjustment = {
2419
+ type: 'brightness/contrast',
2420
+ brightness: desc.Brgh,
2421
+ contrast: desc.Cntr,
2422
+ meanValue: desc.means,
2423
+ useLegacy: !!desc.useLegacy,
2424
+ labColorOnly: !!desc['Lab '],
2425
+ auto: !!desc.Auto,
2426
+ };
2427
+ }
2428
+
2429
+ skipBytes(reader, left());
2430
+ },
2431
+ (writer, target) => {
2432
+ const info = target.adjustment!;
2433
+
2434
+ if (info.type === 'levels' || info.type === 'exposure' || info.type === 'hue/saturation') {
2435
+ const desc: PresetDescriptor = {
2436
+ Vrsn: 1,
2437
+ presetKind: info.presetKind ?? 1,
2438
+ presetFileName: info.presetFileName || '',
2439
+ };
2440
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2441
+ } else if (info.type === 'curves') {
2442
+ const desc: CurvesPresetDescriptor = {
2443
+ Vrsn: 1,
2444
+ curvesPresetKind: info.presetKind ?? 1,
2445
+ curvesPresetFileName: info.presetFileName || '',
2446
+ };
2447
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2448
+ } else if (info.type === 'channel mixer') {
2449
+ const desc: MixerPresetDescriptor = {
2450
+ Vrsn: 1,
2451
+ mixerPresetKind: info.presetKind ?? 1,
2452
+ mixerPresetFileName: info.presetFileName || '',
2453
+ };
2454
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2455
+ } else if (info.type === 'brightness/contrast') {
2456
+ const desc: BrightnessContrastDescriptor = {
2457
+ Vrsn: 1,
2458
+ Brgh: info.brightness || 0,
2459
+ Cntr: info.contrast || 0,
2460
+ means: info.meanValue ?? 127,
2461
+ 'Lab ': !!info.labColorOnly,
2462
+ useLegacy: !!info.useLegacy,
2463
+ Auto: !!info.auto,
2464
+ };
2465
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2466
+ } else {
2467
+ throw new Error('Unhandled CgEd case');
2468
+ }
2469
+ },
2470
+ );
2471
+
2472
+ addHandler(
2473
+ 'Txt2',
2474
+ hasKey('engineData'),
2475
+ (reader, target, left) => {
2476
+ const data = readBytes(reader, left());
2477
+ target.engineData = fromByteArray(data);
2478
+ // const engineData = parseEngineData(data);
2479
+ // console.log(require('util').inspect(engineData, false, 99, true));
2480
+ // require('fs').writeFileSync('resources/engineData2Simple.txt', require('util').inspect(engineData, false, 99, false), 'utf8');
2481
+ // require('fs').writeFileSync('test_data.json', JSON.stringify(ed, null, 2), 'utf8');
2482
+ },
2483
+ (writer, target) => {
2484
+ const buffer = toByteArray(target.engineData!);
2485
+ writeBytes(writer, buffer);
2486
+ },
2487
+ );
2488
+
2489
+ addHandler(
2490
+ 'FMsk',
2491
+ hasKey('filterMask'),
2492
+ (reader, target) => {
2493
+ target.filterMask = {
2494
+ colorSpace: readColor(reader),
2495
+ opacity: readUint16(reader) / 0xff,
2496
+ };
2497
+ },
2498
+ (writer, target) => {
2499
+ writeColor(writer, target.filterMask!.colorSpace);
2500
+ writeUint16(writer, clamp(target.filterMask!.opacity ?? 1, 0, 1) * 0xff);
2501
+ },
2502
+ );
2503
+
2504
+ interface ArtdDescriptor {
2505
+ 'Cnt ': number;
2506
+ autoExpandOffset: { Hrzn: number; Vrtc: number; };
2507
+ origin: { Hrzn: number; Vrtc: number; };
2508
+ autoExpandEnabled: boolean;
2509
+ autoNestEnabled: boolean;
2510
+ autoPositionEnabled: boolean;
2511
+ shrinkwrapOnSaveEnabled: boolean;
2512
+ docDefaultNewArtboardBackgroundColor: DescriptorColor;
2513
+ docDefaultNewArtboardBackgroundType: number;
2514
+ }
2515
+
2516
+ addHandler(
2517
+ 'artd', // document-wide artboard info
2518
+ target => (target as Psd).artboards !== undefined,
2519
+ (reader, target, left) => {
2520
+ const desc = readVersionAndDescriptor(reader) as ArtdDescriptor;
2521
+ (target as Psd).artboards = {
2522
+ count: desc['Cnt '],
2523
+ autoExpandOffset: { horizontal: desc.autoExpandOffset.Hrzn, vertical: desc.autoExpandOffset.Vrtc },
2524
+ origin: { horizontal: desc.origin.Hrzn, vertical: desc.origin.Vrtc },
2525
+ autoExpandEnabled: desc.autoExpandEnabled,
2526
+ autoNestEnabled: desc.autoNestEnabled,
2527
+ autoPositionEnabled: desc.autoPositionEnabled,
2528
+ shrinkwrapOnSaveEnabled: desc.shrinkwrapOnSaveEnabled,
2529
+ docDefaultNewArtboardBackgroundColor: parseColor(desc.docDefaultNewArtboardBackgroundColor),
2530
+ docDefaultNewArtboardBackgroundType: desc.docDefaultNewArtboardBackgroundType,
2531
+ };
2532
+
2533
+ skipBytes(reader, left());
2534
+ },
2535
+ (writer, target) => {
2536
+ const artb = (target as Psd).artboards!;
2537
+ const desc: ArtdDescriptor = {
2538
+ 'Cnt ': artb.count,
2539
+ autoExpandOffset: artb.autoExpandOffset ? { Hrzn: artb.autoExpandOffset.horizontal, Vrtc: artb.autoExpandOffset.vertical } : { Hrzn: 0, Vrtc: 0 },
2540
+ origin: artb.origin ? { Hrzn: artb.origin.horizontal, Vrtc: artb.origin.vertical } : { Hrzn: 0, Vrtc: 0 },
2541
+ autoExpandEnabled: artb.autoExpandEnabled ?? true,
2542
+ autoNestEnabled: artb.autoNestEnabled ?? true,
2543
+ autoPositionEnabled: artb.autoPositionEnabled ?? true,
2544
+ shrinkwrapOnSaveEnabled: artb.shrinkwrapOnSaveEnabled ?? true,
2545
+ docDefaultNewArtboardBackgroundColor: serializeColor(artb.docDefaultNewArtboardBackgroundColor),
2546
+ docDefaultNewArtboardBackgroundType: artb.docDefaultNewArtboardBackgroundType ?? 1,
2547
+ };
2548
+ writeVersionAndDescriptor(writer, '', 'null', desc, 'artd');
2549
+ },
2550
+ );
2551
+
2552
+ export function hasMultiEffects(effects: LayerEffectsInfo) {
2553
+ return Object.keys(effects).map(key => (effects as any)[key]).some(v => Array.isArray(v) && v.length > 1);
2554
+ }
2555
+
2556
+ addHandler(
2557
+ 'lfx2',
2558
+ target => target.effects !== undefined && !hasMultiEffects(target.effects),
2559
+ (reader, target, left, _, options) => {
2560
+ const version = readUint32(reader);
2561
+ if (version !== 0) throw new Error(`Invalid lfx2 version`);
2562
+
2563
+ const desc: Lfx2Descriptor = readVersionAndDescriptor(reader);
2564
+ // console.log(require('util').inspect(desc, false, 99, true));
2565
+
2566
+ // TODO: don't discard if we got it from lmfx
2567
+ // discard if read in 'lrFX' section
2568
+ target.effects = parseEffects(desc, !!options.logMissingFeatures);
2569
+
2570
+ skipBytes(reader, left());
2571
+ },
2572
+ (writer, target, _, options) => {
2573
+ const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, false);
2574
+ // console.log(require('util').inspect(desc, false, 99, true));
2575
+
2576
+ writeUint32(writer, 0); // version
2577
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2578
+ },
2579
+ );
2580
+
2581
+ interface CinfDescriptor {
2582
+ Vrsn: { major: number; minor: number; fix: number; };
2583
+ psVersion?: { major: number; minor: number; fix: number; };
2584
+ description: string;
2585
+ reason: string;
2586
+ Engn: string; // 'Engn.compCore';
2587
+ enableCompCore: string; // 'enable.feature';
2588
+ enableCompCoreGPU: string; // 'enable.feature';
2589
+ enableCompCoreThreads?: string; // 'enable.feature';
2590
+ compCoreSupport: string; // 'reason.supported';
2591
+ compCoreGPUSupport: string; // 'reason.featureDisabled';
2592
+ }
2593
+
2594
+ addHandler(
2595
+ 'cinf',
2596
+ hasKey('compositorUsed'),
2597
+ (reader, target, left) => {
2598
+ const desc = readVersionAndDescriptor(reader) as CinfDescriptor;
2599
+ // console.log(require('util').inspect(desc, false, 99, true));
2600
+
2601
+ target.compositorUsed = {
2602
+ description: desc.description,
2603
+ reason: desc.reason,
2604
+ engine: desc.Engn.split('.')[1],
2605
+ enableCompCore: desc.enableCompCore.split('.')[1],
2606
+ enableCompCoreGPU: desc.enableCompCoreGPU.split('.')[1],
2607
+ compCoreSupport: desc.compCoreSupport.split('.')[1],
2608
+ compCoreGPUSupport: desc.compCoreGPUSupport.split('.')[1],
2609
+ };
2610
+
2611
+ skipBytes(reader, left());
2612
+ },
2613
+ (writer, target) => {
2614
+ const cinf = target.compositorUsed!;
2615
+ const desc: CinfDescriptor = {
2616
+ Vrsn: { major: 1, minor: 0, fix: 0 }, // TEMP
2617
+ // psVersion: { major: 22, minor: 3, fix: 1 }, // TESTING
2618
+ description: cinf.description,
2619
+ reason: cinf.reason,
2620
+ Engn: `Engn.${cinf.engine}`,
2621
+ enableCompCore: `enable.${cinf.enableCompCore}`,
2622
+ enableCompCoreGPU: `enable.${cinf.enableCompCoreGPU}`,
2623
+ // enableCompCoreThreads: `enable.feature`, // TESTING
2624
+ compCoreSupport: `reason.${cinf.compCoreSupport}`,
2625
+ compCoreGPUSupport: `reason.${cinf.compCoreGPUSupport}`,
2626
+ };
2627
+ writeVersionAndDescriptor(writer, '', 'null', desc);
2628
+ },
2629
+ );
2630
+
2631
+ // extension settings ?, ignore it
2632
+ addHandler(
2633
+ 'extn',
2634
+ target => (target as any)._extn !== undefined,
2635
+ (reader, target) => {
2636
+ const desc: ExtensionDesc = readVersionAndDescriptor(reader);
2637
+ // console.log(require('util').inspect(desc, false, 99, true));
2638
+
2639
+ if (MOCK_HANDLERS) (target as any)._extn = desc;
2640
+ },
2641
+ (writer, target) => {
2642
+ // TODO: need to add correct types for desc fields (resources/src.psd)
2643
+ if (MOCK_HANDLERS) writeVersionAndDescriptor(writer, '', 'null', (target as any)._extn);
2644
+ },
2645
+ );
2646
+
2647
+ addHandler(
2648
+ 'iOpa',
2649
+ hasKey('fillOpacity'),
2650
+ (reader, target) => {
2651
+ target.fillOpacity = readUint8(reader) / 0xff;
2652
+ skipBytes(reader, 3);
2653
+ },
2654
+ (writer, target) => {
2655
+ writeUint8(writer, target.fillOpacity! * 0xff);
2656
+ writeZeros(writer, 3);
2657
+ },
2658
+ );
2659
+
2660
+ addHandler(
2661
+ 'brst',
2662
+ hasKey('channelBlendingRestrictions'),
2663
+ (reader, target, left) => {
2664
+ target.channelBlendingRestrictions = [];
2665
+
2666
+ while (left() > 4) {
2667
+ target.channelBlendingRestrictions.push(readInt32(reader));
2668
+ }
2669
+ },
2670
+ (writer, target) => {
2671
+ for (const channel of target.channelBlendingRestrictions!) {
2672
+ writeInt32(writer, channel);
2673
+ }
2674
+ },
2675
+ );
2676
+
2677
+ addHandler(
2678
+ 'tsly',
2679
+ hasKey('transparencyShapesLayer'),
2680
+ (reader, target) => {
2681
+ target.transparencyShapesLayer = !!readUint8(reader);
2682
+ skipBytes(reader, 3);
2683
+ },
2684
+ (writer, target) => {
2685
+ writeUint8(writer, target.transparencyShapesLayer ? 1 : 0);
2686
+ writeZeros(writer, 3);
2687
+ },
2688
+ );