ag-psd 32.0.0 → 34.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  22. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  23. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  24. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  25. package/CHANGELOG.md +100 -0
  26. package/LICENSE +5 -0
  27. package/README.md +4 -7
  28. package/README_PSD.md +261 -84
  29. package/TODO.md +13 -84
  30. package/dist/abr.d.ts +11 -4
  31. package/dist/abr.js +362 -230
  32. package/dist/abr.js.map +1 -1
  33. package/dist/additionalInfo.d.ts +56 -2
  34. package/dist/additionalInfo.js +828 -361
  35. package/dist/additionalInfo.js.map +1 -1
  36. package/dist/ase.d.ts +6 -1
  37. package/dist/ase.js +61 -61
  38. package/dist/ase.js.map +1 -1
  39. package/dist/bundle.js +3321 -1364
  40. package/dist/descriptor.d.ts +28 -13
  41. package/dist/descriptor.js +295 -241
  42. package/dist/descriptor.js.map +1 -1
  43. package/dist/helpers.d.ts +7 -1
  44. package/dist/helpers.js +246 -4
  45. package/dist/helpers.js.map +1 -1
  46. package/dist/imageResources.d.ts +15 -3
  47. package/dist/imageResources.js +586 -144
  48. package/dist/imageResources.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/jpeg.js +8 -8
  53. package/dist/jpeg.js.map +1 -1
  54. package/dist/psd.d.ts +292 -19
  55. package/dist/psd.js.map +1 -1
  56. package/dist/psdReader.d.ts +10 -0
  57. package/dist/psdReader.js +424 -216
  58. package/dist/psdReader.js.map +1 -1
  59. package/dist/psdWriter.d.ts +2 -0
  60. package/dist/psdWriter.js +373 -113
  61. package/dist/psdWriter.js.map +1 -1
  62. package/dist/text.d.ts +32 -1
  63. package/dist/text.js +75 -6
  64. package/dist/text.js.map +1 -1
  65. package/dist/utf8.js +0 -3
  66. package/dist/utf8.js.map +1 -1
  67. package/dist-es/abr.d.ts +11 -4
  68. package/dist-es/abr.js +362 -230
  69. package/dist-es/abr.js.map +1 -1
  70. package/dist-es/additionalInfo.d.ts +56 -2
  71. package/dist-es/additionalInfo.js +828 -361
  72. package/dist-es/additionalInfo.js.map +1 -1
  73. package/dist-es/ase.d.ts +6 -1
  74. package/dist-es/ase.js +61 -61
  75. package/dist-es/ase.js.map +1 -1
  76. package/dist-es/descriptor.d.ts +28 -13
  77. package/dist-es/descriptor.js +295 -241
  78. package/dist-es/descriptor.js.map +1 -1
  79. package/dist-es/helpers.d.ts +7 -1
  80. package/dist-es/helpers.js +246 -4
  81. package/dist-es/helpers.js.map +1 -1
  82. package/dist-es/imageResources.d.ts +15 -3
  83. package/dist-es/imageResources.js +586 -144
  84. package/dist-es/imageResources.js.map +1 -1
  85. package/dist-es/index.d.ts +1 -0
  86. package/dist-es/index.js +1 -0
  87. package/dist-es/index.js.map +1 -1
  88. package/dist-es/jpeg.js +8 -8
  89. package/dist-es/jpeg.js.map +1 -1
  90. package/dist-es/psd.d.ts +292 -19
  91. package/dist-es/psd.js.map +1 -1
  92. package/dist-es/psdReader.d.ts +10 -0
  93. package/dist-es/psdReader.js +424 -216
  94. package/dist-es/psdReader.js.map +1 -1
  95. package/dist-es/psdWriter.d.ts +2 -0
  96. package/dist-es/psdWriter.js +373 -113
  97. package/dist-es/psdWriter.js.map +1 -1
  98. package/dist-es/text.d.ts +32 -1
  99. package/dist-es/text.js +75 -6
  100. package/dist-es/text.js.map +1 -1
  101. package/dist-es/utf8.js +0 -3
  102. package/dist-es/utf8.js.map +1 -1
  103. package/package.json +2 -2
  104. package/perf.js +312 -9
  105. package/src/abr.ts +382 -235
  106. package/src/additionalInfo.ts +1071 -482
  107. package/src/ase.ts +66 -61
  108. package/src/descriptor.ts +291 -242
  109. package/src/helpers.ts +266 -3
  110. package/src/imageResources.ts +736 -169
  111. package/src/index.ts +1 -0
  112. package/src/jpeg.ts +8 -8
  113. package/src/psd.ts +271 -32
  114. package/src/psdReader.ts +440 -221
  115. package/src/psdWriter.ts +402 -110
  116. package/src/text.ts +93 -7
  117. package/src/utf8.ts +0 -4
@@ -1,16 +1,18 @@
1
1
  import { fromByteArray, toByteArray } from 'base64-js';
2
2
  import { readEffects, writeEffects } from './effectsHelpers';
3
3
  import { clamp, createEnum, layerColors, MOCK_HANDLERS, normalizeLabAB } from './helpers';
4
- import { LayerAdditionalInfo, BezierPath, Psd, BrightnessAdjustment, ExposureAdjustment, VibranceAdjustment, ColorBalanceAdjustment, BlackAndWhiteAdjustment, PhotoFilterAdjustment, ChannelMixerChannel, ChannelMixerAdjustment, PosterizeAdjustment, ThresholdAdjustment, GradientMapAdjustment, CMYK, SelectiveColorAdjustment, ColorLookupAdjustment, LevelsAdjustmentChannel, LevelsAdjustment, CurvesAdjustment, CurvesAdjustmentChannel, HueSaturationAdjustment, HueSaturationAdjustmentChannel, PresetInfo, Color, ColorBalanceValues, WriteOptions, LinkedFile, PlacedLayerType, Warp, KeyDescriptorItem, BooleanOperation, LayerEffectsInfo, Annotation, LayerVectorMask, AnimationFrame, Timeline, PlacedLayerFilter, UnitsValue, Filter, PlacedLayer, ReadOptions, Layer } from './psd';
5
- import { PsdReader, readSignature, readUnicodeString, skipBytes, readUint32, readUint8, readFloat64, readUint16, readBytes, readInt16, checkSignature, readFloat32, readFixedPointPath32, readSection, readColor, readInt32, readPascalString, readUnicodeStringWithLength, readAsciiString, readPattern, readLayerInfo } from './psdReader';
4
+ import { LayerAdditionalInfo, LevelsFilterAdjustment, BezierPath, Psd, BrightnessAdjustment, ExposureAdjustment, VibranceAdjustment, ColorBalanceAdjustment, BlackAndWhiteAdjustment, PhotoFilterAdjustment, ChannelMixerChannel, ChannelMixerAdjustment, PosterizeAdjustment, ThresholdAdjustment, GradientMapAdjustment, CMYK, SelectiveColorAdjustment, ColorLookupAdjustment, LevelsAdjustmentChannel, LevelsAdjustment, CurvesAdjustment, CurvesAdjustmentChannel, HueSaturationAdjustment, HueSaturationAdjustmentChannel, PresetInfo, Color, ColorBalanceValues, WriteOptions, LinkedFile, PlacedLayerType, Warp, KeyDescriptorItem, BooleanOperation, LayerEffectsInfo, Annotation, VectorPathData, AnimationFrame, Timeline, PlacedLayerFilter, UnitsValue, Filter, PlacedLayer, Layer, CameraRawColors, CameraRawCurvePoint, CameraRawFilterSettings } from './psd';
5
+ import { PsdReader, readSignature, readUnicodeString, skipBytes, readUint32, readUint8, readFloat64, readUint16, readBytes, readInt16, checkSignature, readFloat32, readFixedPointPath32, readSection, readColor, readInt32, readPascalString, readUnicodeStringWithLength, readAsciiString, readPatternSafe, readLayerInfo } from './psdReader';
6
6
  import { PsdWriter, writeZeros, writeSignature, writeBytes, writeUint32, writeUint16, writeFloat64, writeUint8, writeInt16, writeFloat32, writeFixedPointPath32, writeUnicodeString, writeSection, writeUnicodeStringWithPadding, writeColor, writePascalString, writeInt32, writePattern, writeUint64 } from './psdWriter';
7
- import { Annt, BlnM, DescriptorColor, DescriptorUnitsValue, parsePercent, parseUnits, parseUnitsOrNumber, QuiltWarpDescriptor, strokeStyleLineAlignment, strokeStyleLineCapType, strokeStyleLineJoinType, TextDescriptor, textGridding, unitsPercent, unitsValue, WarpDescriptor, warpStyle, writeVersionAndDescriptor, readVersionAndDescriptor, StrokeDescriptor, Ornt, horzVrtcToXY, LmfxDescriptor, Lfx2Descriptor, FrameListDescriptor, TimelineDescriptor, FrameDescriptor, xyToHorzVrtc, serializeEffects, parseEffects, parseColor, serializeColor, serializeVectorContent, parseVectorContent, parseTrackList, serializeTrackList, FractionDescriptor, BlrM, BlrQ, SmBQ, SmBM, DspM, UndA, Cnvr, RplS, SphM, Wvtp, ZZTy, Dstr, Chnl, MztT, Lns, blurType, DfsM, ExtT, ExtR, FlCl, CntE, WndM, Drct, IntE, IntC, FlMd, unitsPercentF, frac, ClrS, descBoundsToBounds, boundsToDescBounds, presetKindType, gradientInterpolationMethodType, PxScDescriptor } from './descriptor';
7
+ import { Annt, BlnM, DescriptorColor, DescriptorUnitsValue, parsePercent, parseUnits, parseUnitsOrNumber, QuiltWarpDescriptor, strokeStyleLineAlignment, strokeStyleLineCapType, strokeStyleLineJoinType, TextDescriptor, textGridding, unitsPercent, unitsValue, WarpDescriptor, warpStyle, writeVersionAndDescriptor, readVersionAndDescriptor, StrokeDescriptor, Ornt, horzVrtcToXY, LmfxDescriptor, Lfx2Descriptor, FrameListDescriptor, TimelineDescriptor, FrameDescriptor, xyToHorzVrtc, serializeEffects, parseEffects, parseColor, serializeColor, serializeVectorContent, parseVectorContent, parseTrackList, serializeTrackList, FractionDescriptor, BlrM, BlrQ, SmBQ, SmBM, DspM, UndA, Cnvr, RplS, SphM, Wvtp, ZZTy, Dstr, Chnl, MztT, Lns, blurType, DfsM, ExtT, ExtR, FlCl, CntE, WndM, Drct, IntE, IntC, FlMd, unitsPercentF, frac, ClrS, descBoundsToBounds, boundsToDescBounds, presetKindType, gradientInterpolationMethodType, PxScDescriptor, cameraRawFilterClassID, patternColorHandling } from './descriptor';
8
8
  import { serializeEngineData, parseEngineData } from './engineData';
9
- import { encodeEngineData, decodeEngineData } from './text';
9
+ import { encodeEngineData, decodeEngineData, decodeLegacyTextData, LegacyTextData } from './text';
10
10
  import { decodeEngineData2 } from './engineData2';
11
11
  import type { InternalImageResources } from './imageResources';
12
12
 
13
13
  export interface ExtendedWriteOptions extends WriteOptions {
14
+ colorMode?: number; // color mode of written document
15
+ bitsPerChannel?: number; // bit depth of written document
14
16
  layerIds: Set<number>;
15
17
  layerToId: Map<any, number>;
16
18
  explicitLayerIds?: Set<number>; // ids set on layers in the document (generated ids can't use them)
@@ -103,7 +105,8 @@ addHandler(
103
105
  // require('fs').writeFileSync('after.txt', require('util').inspect(after, false, 99, false), 'utf8');
104
106
 
105
107
  // console.log(require('util').inspect(parseEngineData(text.EngineData), false, 99, true));
106
- target.text = { ...target.text, ...textData };
108
+ // descriptor anti-alias value is more reliable than the one from engine data
109
+ target.text = { ...target.text, ...textData, antiAlias: target.text!.antiAlias ?? textData.antiAlias };
107
110
  // console.log(require('util').inspect(target.text, false, 99, true));
108
111
  }
109
112
 
@@ -146,15 +149,95 @@ addHandler(
146
149
  },
147
150
  );
148
151
 
152
+ // type tool info from Photoshop 5.0 and 5.5, converted to regular text data (written as 'TySh')
153
+ addHandler(
154
+ 'tySh',
155
+ () => false,
156
+ (reader, target, left) => {
157
+ if (readInt16(reader) !== 1) throw new Error(`Invalid tySh version`);
158
+
159
+ const transform: number[] = [];
160
+ for (let i = 0; i < 6; i++) transform.push(readFloat64(reader));
161
+
162
+ if (readInt16(reader) !== 6) throw new Error(`Invalid tySh font version`);
163
+
164
+ const data: LegacyTextData = { transform, faces: [], styles: [], lines: [], color: { r: 0, g: 0, b: 0 }, antiAlias: false };
165
+ const facesCount = readInt16(reader);
166
+
167
+ for (let i = 0; i < facesCount; i++) {
168
+ const mark = readInt16(reader);
169
+ readInt32(reader); // font type
170
+ const name = readPascalString(reader, 1);
171
+ const familyName = readPascalString(reader, 1);
172
+ const styleName = readPascalString(reader, 1);
173
+ const script = readInt16(reader);
174
+ const axesCount = readInt32(reader);
175
+ if (axesCount < 0 || axesCount * 4 > left()) throw new Error(`Invalid tySh design axes count`);
176
+ skipBytes(reader, axesCount * 4);
177
+ data.faces.push({ mark, name, familyName, styleName, script });
178
+ }
179
+
180
+ const stylesCount = readInt16(reader);
181
+
182
+ for (let i = 0; i < stylesCount; i++) {
183
+ const mark = readInt16(reader);
184
+ const faceMark = readInt16(reader);
185
+ const size = readInt32(reader);
186
+ const tracking = readInt32(reader);
187
+ const kerning = readInt32(reader);
188
+ const leading = readInt32(reader);
189
+ const baselineShift = readInt32(reader);
190
+ const autoKerning = !!readUint8(reader);
191
+ readUint8(reader); // rotate
192
+ data.styles.push({ mark, faceMark, size, tracking, kerning, leading, baselineShift, autoKerning });
193
+ }
194
+
195
+ readInt16(reader); // type (0 = point, 1 = paragraph)
196
+ readInt32(reader); // scaling factor
197
+ readInt32(reader); // character count
198
+ readInt32(reader); // horizontal placement
199
+ readInt32(reader); // vertical placement
200
+ readInt32(reader); // select start
201
+ readInt32(reader); // select end
202
+
203
+ const linesCount = readInt16(reader);
204
+
205
+ for (let i = 0; i < linesCount; i++) {
206
+ const charCount = readInt32(reader);
207
+ const orientation = readInt16(reader);
208
+ const alignment = readInt16(reader);
209
+ if (charCount < 0 || charCount * 4 > left()) throw new Error(`Invalid tySh character count`);
210
+ const chars: { code: number; style: number; }[] = [];
211
+
212
+ for (let j = 0; j < charCount; j++) {
213
+ const code = readUint16(reader);
214
+ const style = readInt16(reader);
215
+ chars.push({ code, style });
216
+ }
217
+
218
+ data.lines.push({ orientation, alignment, chars });
219
+ }
220
+
221
+ data.color = readColor(reader);
222
+ data.antiAlias = !!readUint8(reader);
223
+
224
+ if (!target.text) target.text = decodeLegacyTextData(data);
225
+
226
+ skipBytes(reader, left());
227
+ },
228
+ () => { },
229
+ );
230
+
149
231
  // vector fills
150
232
 
151
233
  addHandler(
152
234
  'SoCo',
153
235
  target => target.vectorFill !== undefined && target.vectorStroke === undefined &&
154
236
  target.vectorFill.type === 'color',
155
- (reader, target) => {
237
+ (reader, target, left) => {
156
238
  const descriptor = readVersionAndDescriptor(reader);
157
239
  target.vectorFill = parseVectorContent(descriptor);
240
+ skipBytes(reader, left());
158
241
  },
159
242
  (writer, target) => {
160
243
  const { descriptor } = serializeVectorContent(target.vectorFill!);
@@ -228,7 +311,7 @@ function writeBezierKnot(writer: PsdWriter, points: number[], width: number, hei
228
311
 
229
312
  export const booleanOperations: BooleanOperation[] = ['exclude', 'combine', 'subtract', 'intersect'];
230
313
 
231
- export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, width: number, height: number, size: number) {
314
+ export function readVectorMask(reader: PsdReader, vectorMask: VectorPathData, width: number, height: number, size: number) {
232
315
  const end = reader.offset + size;
233
316
  const paths = vectorMask.paths;
234
317
  let path: BezierPath | undefined = undefined;
@@ -283,6 +366,51 @@ export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, w
283
366
  return paths;
284
367
  }
285
368
 
369
+ // componentIndices - write index of path component for each subpath (Photoshop writes them for layer vector masks)
370
+ export function writeVectorMask(writer: PsdWriter, vectorMask: VectorPathData, width: number, height: number, componentIndices = false) {
371
+ // initial entry
372
+ writeUint16(writer, 6);
373
+ writeZeros(writer, 24);
374
+
375
+ const clipboard = vectorMask.clipboard;
376
+ if (clipboard) {
377
+ writeUint16(writer, 7);
378
+ writeFixedPointPath32(writer, clipboard.top);
379
+ writeFixedPointPath32(writer, clipboard.left);
380
+ writeFixedPointPath32(writer, clipboard.bottom);
381
+ writeFixedPointPath32(writer, clipboard.right);
382
+ writeFixedPointPath32(writer, clipboard.resolution);
383
+ writeZeros(writer, 4);
384
+ }
385
+
386
+ writeUint16(writer, 8);
387
+ writeUint16(writer, vectorMask.fillStartsWithAllPixels ? 1 : 0);
388
+ writeZeros(writer, 22);
389
+
390
+ let componentIndex = -1;
391
+
392
+ for (const path of vectorMask.paths) {
393
+ // subpaths without operation are part of the same path component as previous subpath
394
+ if (path.operation || componentIndex < 0) componentIndex++;
395
+
396
+ writeUint16(writer, path.open ? 3 : 0);
397
+ writeUint16(writer, path.knots.length);
398
+ writeUint16(writer, path.operation ? booleanOperations.indexOf(path.operation) : -1); // -1 for undefined
399
+ writeUint16(writer, path.fillRule === 'non-zero' ? 2 : 1);
400
+ writeZeros(writer, 4);
401
+ writeUint32(writer, componentIndices ? componentIndex : 0);
402
+ writeZeros(writer, 10);
403
+
404
+ const linkedKnot = path.open ? 4 : 1;
405
+ const unlinkedKnot = path.open ? 5 : 2;
406
+
407
+ for (const { linked, points } of path.knots) {
408
+ writeUint16(writer, linked ? linkedKnot : unlinkedKnot);
409
+ writeBezierKnot(writer, points, width, height);
410
+ }
411
+ }
412
+ }
413
+
286
414
  addHandler(
287
415
  'vmsk',
288
416
  hasKey('vectorMask'),
@@ -312,45 +440,11 @@ addHandler(
312
440
 
313
441
  writeUint32(writer, 3); // version
314
442
  writeUint32(writer, flags);
315
-
316
- // initial entry
317
- writeUint16(writer, 6);
318
- writeZeros(writer, 24);
319
-
320
- const clipboard = vectorMask.clipboard;
321
- if (clipboard) {
322
- writeUint16(writer, 7);
323
- writeFixedPointPath32(writer, clipboard.top);
324
- writeFixedPointPath32(writer, clipboard.left);
325
- writeFixedPointPath32(writer, clipboard.bottom);
326
- writeFixedPointPath32(writer, clipboard.right);
327
- writeFixedPointPath32(writer, clipboard.resolution);
328
- writeZeros(writer, 4);
329
- }
330
-
331
- writeUint16(writer, 8);
332
- writeUint16(writer, vectorMask.fillStartsWithAllPixels ? 1 : 0);
333
- writeZeros(writer, 22);
334
-
335
- for (const path of vectorMask.paths) {
336
- writeUint16(writer, path.open ? 3 : 0);
337
- writeUint16(writer, path.knots.length);
338
- writeUint16(writer, path.operation ? booleanOperations.indexOf(path.operation) : -1); // -1 for undefined
339
- writeUint16(writer, path.fillRule === 'non-zero' ? 2 : 1);
340
- writeZeros(writer, 18); // TODO: these are sometimes non-zero
341
-
342
- const linkedKnot = path.open ? 4 : 1;
343
- const unlinkedKnot = path.open ? 5 : 2;
344
-
345
- for (const { linked, points } of path.knots) {
346
- writeUint16(writer, linked ? linkedKnot : unlinkedKnot);
347
- writeBezierKnot(writer, points, width, height);
348
- }
349
- }
443
+ writeVectorMask(writer, vectorMask, width, height, true);
350
444
  },
351
445
  );
352
446
 
353
- // TODO: need to write vmsk if has outline ?
447
+ // written instead of 'vmsk' for layers with vector stroke ('vscg' section)
354
448
  addHandlerAlias('vsms', 'vmsk');
355
449
  // addHandlerAlias('vmsk', 'vsms');
356
450
 
@@ -365,7 +459,7 @@ addHandler(
365
459
  },
366
460
  );
367
461
 
368
- interface VogkDescriptor {
462
+ export interface KeyDescriptorListDescriptor {
369
463
  keyDescriptorList: {
370
464
  keyShapeInvalidated?: boolean;
371
465
  keyOriginType?: number;
@@ -395,121 +489,128 @@ interface VogkDescriptor {
395
489
  }[];
396
490
  }
397
491
 
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: [] };
492
+ export function parseKeyDescriptorList(desc: KeyDescriptorListDescriptor) {
493
+ const keyDescriptorList: KeyDescriptorItem[] = [];
494
+
495
+ for (const i of desc.keyDescriptorList) {
496
+ const item: KeyDescriptorItem = {};
497
+
498
+ if (i.keyShapeInvalidated != null) item.keyShapeInvalidated = i.keyShapeInvalidated;
499
+ if (i.keyOriginType != null) item.keyOriginType = i.keyOriginType;
500
+ if (i.keyOriginResolution != null) item.keyOriginResolution = i.keyOriginResolution;
501
+ if (i.keyOriginShapeBBox) {
502
+ item.keyOriginShapeBoundingBox = {
503
+ top: parseUnitsOrNumber(i.keyOriginShapeBBox['Top ']),
504
+ left: parseUnitsOrNumber(i.keyOriginShapeBBox.Left),
505
+ bottom: parseUnitsOrNumber(i.keyOriginShapeBBox.Btom),
506
+ right: parseUnitsOrNumber(i.keyOriginShapeBBox.Rght),
507
+ };
508
+ }
509
+ const rectRadii = i.keyOriginRRectRadii;
510
+ if (rectRadii) {
511
+ item.keyOriginRRectRadii = {
512
+ topRight: parseUnits(rectRadii.topRight),
513
+ topLeft: parseUnits(rectRadii.topLeft),
514
+ bottomLeft: parseUnits(rectRadii.bottomLeft),
515
+ bottomRight: parseUnits(rectRadii.bottomRight),
516
+ };
517
+ }
518
+ const corners = i.keyOriginBoxCorners;
519
+ if (corners) {
520
+ item.keyOriginBoxCorners = [
521
+ { x: corners.rectangleCornerA.Hrzn, y: corners.rectangleCornerA.Vrtc },
522
+ { x: corners.rectangleCornerB.Hrzn, y: corners.rectangleCornerB.Vrtc },
523
+ { x: corners.rectangleCornerC.Hrzn, y: corners.rectangleCornerC.Vrtc },
524
+ { x: corners.rectangleCornerD.Hrzn, y: corners.rectangleCornerD.Vrtc },
525
+ ];
526
+ }
527
+ const trnf = i.Trnf;
528
+ if (trnf) {
529
+ item.transform = [trnf.xx, trnf.xy, trnf.yx, trnf.yy, trnf.tx, trnf.ty];
530
+ }
407
531
 
408
- for (const i of desc.keyDescriptorList) {
409
- const item: KeyDescriptorItem = {};
532
+ keyDescriptorList.push(item);
533
+ }
410
534
 
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: parseUnitsOrNumber(i.keyOriginShapeBBox['Top ']),
417
- left: parseUnitsOrNumber(i.keyOriginShapeBBox.Left),
418
- bottom: parseUnitsOrNumber(i.keyOriginShapeBBox.Btom),
419
- right: parseUnitsOrNumber(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.yx, trnf.yy, trnf.tx, trnf.ty];
443
- }
535
+ return keyDescriptorList;
536
+ }
444
537
 
445
- target.vectorOrigination.keyDescriptorList.push(item);
446
- }
538
+ export function serializeKeyDescriptorList(keyDescriptorList: KeyDescriptorItem[]) {
539
+ const desc: KeyDescriptorListDescriptor = { keyDescriptorList: [] };
447
540
 
448
- skipBytes(reader, left());
449
- },
450
- (writer, target) => {
451
- target;
452
- const orig = target.vectorOrigination!;
453
- const desc: VogkDescriptor = { keyDescriptorList: [] };
541
+ for (let i = 0; i < keyDescriptorList.length; i++) {
542
+ const item = keyDescriptorList[i];
454
543
 
455
- for (let i = 0; i < orig.keyDescriptorList.length; i++) {
456
- const item = orig.keyDescriptorList[i];
544
+ desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
457
545
 
458
- desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
546
+ const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1];
459
547
 
460
- const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1];
548
+ if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType;
549
+ if (item.keyOriginResolution != null) out.keyOriginResolution = item.keyOriginResolution;
461
550
 
462
- if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType;
463
- if (item.keyOriginResolution != null) out.keyOriginResolution = item.keyOriginResolution;
551
+ const radii = item.keyOriginRRectRadii;
552
+ if (radii) {
553
+ out.keyOriginRRectRadii = {
554
+ unitValueQuadVersion: 1,
555
+ topRight: unitsValue(radii.topRight, 'topRight'),
556
+ topLeft: unitsValue(radii.topLeft, 'topLeft'),
557
+ bottomLeft: unitsValue(radii.bottomLeft, 'bottomLeft'),
558
+ bottomRight: unitsValue(radii.bottomRight, 'bottomRight'),
559
+ };
560
+ }
464
561
 
465
- const radii = item.keyOriginRRectRadii;
466
- if (radii) {
467
- out.keyOriginRRectRadii = {
468
- unitValueQuadVersion: 1,
469
- topRight: unitsValue(radii.topRight, 'topRight'),
470
- topLeft: unitsValue(radii.topLeft, 'topLeft'),
471
- bottomLeft: unitsValue(radii.bottomLeft, 'bottomLeft'),
472
- bottomRight: unitsValue(radii.bottomRight, 'bottomRight'),
473
- };
474
- }
562
+ const box = item.keyOriginShapeBoundingBox;
563
+ if (box) {
564
+ out.keyOriginShapeBBox = {
565
+ unitValueQuadVersion: 1,
566
+ 'Top ': unitsValue(box.top, 'top'),
567
+ Left: unitsValue(box.left, 'left'),
568
+ Btom: unitsValue(box.bottom, 'bottom'),
569
+ Rght: unitsValue(box.right, 'right'),
570
+ };
571
+ }
475
572
 
476
- const box = item.keyOriginShapeBoundingBox;
477
- if (box) {
478
- out.keyOriginShapeBBox = {
479
- unitValueQuadVersion: 1,
480
- 'Top ': unitsValue(box.top, 'top'),
481
- Left: unitsValue(box.left, 'left'),
482
- Btom: unitsValue(box.bottom, 'bottom'),
483
- Rght: unitsValue(box.right, 'right'),
484
- };
485
- }
573
+ const corners = item.keyOriginBoxCorners;
574
+ if (corners && corners.length === 4) {
575
+ out.keyOriginBoxCorners = {
576
+ rectangleCornerA: { Hrzn: corners[0].x, Vrtc: corners[0].y },
577
+ rectangleCornerB: { Hrzn: corners[1].x, Vrtc: corners[1].y },
578
+ rectangleCornerC: { Hrzn: corners[2].x, Vrtc: corners[2].y },
579
+ rectangleCornerD: { Hrzn: corners[3].x, Vrtc: corners[3].y },
580
+ };
581
+ }
486
582
 
487
- const corners = item.keyOriginBoxCorners;
488
- if (corners && corners.length === 4) {
489
- out.keyOriginBoxCorners = {
490
- rectangleCornerA: { Hrzn: corners[0].x, Vrtc: corners[0].y },
491
- rectangleCornerB: { Hrzn: corners[1].x, Vrtc: corners[1].y },
492
- rectangleCornerC: { Hrzn: corners[2].x, Vrtc: corners[2].y },
493
- rectangleCornerD: { Hrzn: corners[3].x, Vrtc: corners[3].y },
494
- };
495
- }
583
+ const transform = item.transform;
584
+ if (transform && transform.length === 6) {
585
+ out.Trnf = {
586
+ xx: transform[0],
587
+ xy: transform[1],
588
+ yx: transform[2],
589
+ yy: transform[3],
590
+ tx: transform[4],
591
+ ty: transform[5],
592
+ };
593
+ }
496
594
 
497
- const transform = item.transform;
498
- if (transform && transform.length === 6) {
499
- out.Trnf = {
500
- xx: transform[0],
501
- xy: transform[1],
502
- yx: transform[2],
503
- yy: transform[3],
504
- tx: transform[4],
505
- ty: transform[5],
506
- };
507
- }
595
+ if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated;
596
+ out.keyOriginIndex = i;
597
+ }
508
598
 
509
- if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated;
510
- out.keyOriginIndex = i;
511
- }
599
+ return desc;
600
+ }
512
601
 
602
+ addHandler(
603
+ 'vogk',
604
+ hasKey('vectorOrigination'),
605
+ (reader, target, left) => {
606
+ if (readInt32(reader) !== 1) throw new Error(`Invalid vogk version`);
607
+ const desc = readVersionAndDescriptor(reader) as KeyDescriptorListDescriptor;
608
+ // console.log(require('util').inspect(desc, false, 99, true));
609
+ target.vectorOrigination = { keyDescriptorList: parseKeyDescriptorList(desc) };
610
+ skipBytes(reader, left());
611
+ },
612
+ (writer, target) => {
613
+ const desc = serializeKeyDescriptorList(target.vectorOrigination!.keyDescriptorList);
513
614
  writeInt32(writer, 1); // version
514
615
  writeVersionAndDescriptor(writer, '', 'null', desc);
515
616
  }
@@ -517,7 +618,7 @@ addHandler(
517
618
 
518
619
  addHandler(
519
620
  'lmfx',
520
- target => target.effects !== undefined && hasMultiEffects(target.effects),
621
+ target => target.effects !== undefined && !isGroupLayer(target) && hasMultiEffects(target.effects),
521
622
  (reader, target, left) => {
522
623
  const version = readUint32(reader);
523
624
  if (version !== 0) throw new Error('Invalid lmfx version');
@@ -526,12 +627,12 @@ addHandler(
526
627
  // console.log('READ', require('util').inspect(desc, false, 99, true));
527
628
 
528
629
  // discard if read in 'lrFX' or 'lfx2' section
529
- target.effects = parseEffects(desc, !!reader.logMissingFeatures);
630
+ target.effects = parseEffects(desc, reader);
530
631
 
531
632
  skipBytes(reader, left());
532
633
  },
533
- (writer, target, _, options) => {
534
- const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true);
634
+ (writer, target) => {
635
+ const desc = serializeEffects(target.effects!, writer, true);
535
636
  // console.log('WRITE', require('util').inspect(desc, false, 99, true));
536
637
 
537
638
  writeUint32(writer, 0); // version
@@ -621,7 +722,12 @@ addHandler(
621
722
 
622
723
  if (target.id !== undefined) {
623
724
  id = target.id;
624
- while (options.layerIds.has(id)) id += 100; // make sure we don't have duplicate layer ids
725
+
726
+ if (options.layerIds.has(id)) {
727
+ // make sure we don't have duplicate layer ids (and don't take id explicitly set on another layer)
728
+ const explicitIds = getExplicitLayerIds(psd, options);
729
+ while (options.layerIds.has(id) || explicitIds.has(id)) id += 100;
730
+ }
625
731
  } else {
626
732
  // generate id for layers that don't have it, if it's referenced by other sections
627
733
  const explicitIds = getExplicitLayerIds(psd, options);
@@ -721,6 +827,82 @@ addHandler(
721
827
  },
722
828
  );
723
829
 
830
+ addHandler(
831
+ 'vmgm',
832
+ hasKey('vectorMaskAsGlobalMask'),
833
+ (reader, target) => {
834
+ target.vectorMaskAsGlobalMask = !!readUint8(reader);
835
+ skipBytes(reader, 3);
836
+ },
837
+ (writer, target) => {
838
+ writeUint8(writer, target.vectorMaskAsGlobalMask ? 1 : 0);
839
+ writeZeros(writer, 3);
840
+ },
841
+ );
842
+
843
+ addHandler(
844
+ 'ffxi',
845
+ hasKey('foreignEffectId'),
846
+ (reader, target) => {
847
+ target.foreignEffectId = readInt32(reader);
848
+ },
849
+ (writer, target) => {
850
+ writeInt32(writer, target.foreignEffectId!);
851
+ },
852
+ );
853
+
854
+ addHandler(
855
+ 'shpa',
856
+ hasKey('patternData'),
857
+ (reader, target, left) => {
858
+ if (readInt32(reader) !== 0) throw new Error(`Invalid shpa version`);
859
+
860
+ const count = readInt32(reader);
861
+ target.patternData = [];
862
+
863
+ for (let i = 0; i < count && left() > 0; i++) {
864
+ const signature = readSignature(reader);
865
+ const key = readSignature(reader);
866
+ const patternsCount = readInt32(reader);
867
+ const copyOnSheetDuplication = !!readUint8(reader);
868
+ skipBytes(reader, 3);
869
+ const patterns: NonNullable<LayerAdditionalInfo['patternData']>[number]['patterns'] = [];
870
+
871
+ for (let j = 0; j < patternsCount && left() > 0; j++) {
872
+ const colorHandling = patternColorHandling.decode('.' + readSignature(reader));
873
+ readPascalString(reader, 1); // name
874
+ const name = readUnicodeString(reader);
875
+ const id = readPascalString(reader, 1);
876
+ patterns.push({ colorHandling, name, id });
877
+ }
878
+
879
+ target.patternData.push({ signature, key, copyOnSheetDuplication, patterns });
880
+ }
881
+
882
+ skipBytes(reader, left());
883
+ },
884
+ (writer, target) => {
885
+ const sets = target.patternData!;
886
+ writeInt32(writer, 0); // version
887
+ writeInt32(writer, sets.length);
888
+
889
+ for (const set of sets) {
890
+ writeSignature(writer, set.signature);
891
+ writeSignature(writer, set.key);
892
+ writeInt32(writer, set.patterns.length);
893
+ writeUint8(writer, set.copyOnSheetDuplication ? 1 : 0);
894
+ writeZeros(writer, 3);
895
+
896
+ for (const pattern of set.patterns) {
897
+ writeSignature(writer, patternColorHandling.encode(pattern.colorHandling).split('.')[1]);
898
+ writePascalString(writer, pattern.name.replace(/[^\x20-\x7e]/g, '?'), 1);
899
+ writeUnicodeString(writer, pattern.name);
900
+ writePascalString(writer, pattern.id, 1);
901
+ }
902
+ }
903
+ },
904
+ );
905
+
724
906
  addHandler(
725
907
  'lspf',
726
908
  hasKey('protected'),
@@ -733,15 +915,17 @@ addHandler(
733
915
  };
734
916
 
735
917
  if (flags & 0x08) target.protected.artboards = true;
918
+ if (flags & 0x80000000) target.protected.all = true;
736
919
  },
737
920
  (writer, target) => {
738
921
  const flags =
739
922
  (target.protected!.transparency ? 0x01 : 0) |
740
923
  (target.protected!.composite ? 0x02 : 0) |
741
924
  (target.protected!.position ? 0x04 : 0) |
742
- (target.protected!.artboards ? 0x08 : 0);
925
+ (target.protected!.artboards ? 0x08 : 0) |
926
+ (target.protected!.all ? 0x80000000 : 0);
743
927
 
744
- writeUint32(writer, flags);
928
+ writeUint32(writer, flags >>> 0);
745
929
  },
746
930
  );
747
931
 
@@ -764,6 +948,62 @@ interface CustomDescriptor {
764
948
  layerTime?: number;
765
949
  }
766
950
 
951
+ interface GeneratorSettingsDescriptor {
952
+ generatorSettings?: {
953
+ _name: '';
954
+ _classID: string; // 'null' or 'generatorSettings'
955
+ exportAs?: {
956
+ _name: '';
957
+ _classID: 'null';
958
+ exportOption?: string; // JSON
959
+ scaleSettings?: string; // JSON
960
+ };
961
+ generator_45_assets?: { _name: ''; _classID: string; json: string; };
962
+ layerTime?: number;
963
+ crema?: { _name: ''; _classID: string; json: string; };
964
+ };
965
+ }
966
+
967
+ function parseGeneratorSettings(settings: NonNullable<GeneratorSettingsDescriptor['generatorSettings']>) {
968
+ const result: NonNullable<LayerAdditionalInfo['generatorSettings']> = {};
969
+
970
+ if (settings.exportAs) {
971
+ result.exportAs = {};
972
+ if (settings.exportAs.exportOption !== undefined) result.exportAs.exportOption = settings.exportAs.exportOption;
973
+ if (settings.exportAs.scaleSettings !== undefined) result.exportAs.scaleSettings = settings.exportAs.scaleSettings;
974
+ }
975
+
976
+ if (settings.generator_45_assets) result.assets = settings.generator_45_assets.json;
977
+ if (settings.layerTime !== undefined) result.layerTime = settings.layerTime;
978
+ if (settings.crema) result.crema = settings.crema.json;
979
+
980
+ return result;
981
+ }
982
+
983
+ function serializeGeneratorSettings(generatorSettings: NonNullable<LayerAdditionalInfo['generatorSettings']>): GeneratorSettingsDescriptor {
984
+ const settings: GeneratorSettingsDescriptor['generatorSettings'] = {
985
+ _name: '',
986
+ _classID: generatorSettings.assets !== undefined ? 'generatorSettings' : 'null',
987
+ };
988
+
989
+ if (generatorSettings.exportAs) {
990
+ settings.exportAs = { _name: '', _classID: 'null' };
991
+ if (generatorSettings.exportAs.exportOption !== undefined) settings.exportAs.exportOption = generatorSettings.exportAs.exportOption;
992
+ if (generatorSettings.exportAs.scaleSettings !== undefined) settings.exportAs.scaleSettings = generatorSettings.exportAs.scaleSettings;
993
+ }
994
+
995
+ if (generatorSettings.assets !== undefined) settings.generator_45_assets = { _name: '', _classID: 'generatorSettings', json: generatorSettings.assets };
996
+ if (generatorSettings.layerTime !== undefined) settings.layerTime = generatorSettings.layerTime;
997
+ if (generatorSettings.crema !== undefined) settings.crema = { _name: '', _classID: 'null', json: generatorSettings.crema };
998
+
999
+ return { generatorSettings: settings };
1000
+ }
1001
+
1002
+ // document (Psd object) stores generator settings in 'extn' section instead of 'shmd' section
1003
+ function isDocument(target: LayerAdditionalInfo) {
1004
+ return (target as Psd).width !== undefined;
1005
+ }
1006
+
767
1007
  interface CmlsDescriptor {
768
1008
  origFXRefPoint?: { Hrzn: number; Vrtc: number; };
769
1009
  LyrI: number;
@@ -777,112 +1017,113 @@ interface CmlsDescriptor {
777
1017
 
778
1018
  addHandler(
779
1019
  'shmd', // Metadata setting
780
- target => target.timestamp !== undefined || target.animationFrames !== undefined || target.animationFrameFlags !== undefined || target.timeline !== undefined || target.comps !== undefined,
1020
+ target => target.timestamp !== undefined || target.animationFrames !== undefined || target.animationFrameFlags !== undefined || target.timeline !== undefined || target.comps !== undefined ||
1021
+ (target.generatorSettings !== undefined && !isDocument(target)),
781
1022
  (reader, target, left) => {
782
1023
  const count = readUint32(reader);
783
1024
 
784
1025
  for (let i = 0; i < count; i++) {
785
- checkSignature(reader, '8BIM');
1026
+ const signature = readSignature(reader);
1027
+ if (signature !== '8BIM' && signature !== '8ELE') throw new Error(`Invalid shmd signature: '${signature}'`); // 8ELE in Photoshop Elements
786
1028
  const key = readSignature(reader);
787
1029
  readUint8(reader); // copy
788
1030
  skipBytes(reader, 3);
789
1031
 
790
- readSection(reader, 1, left => {
791
- if (key === 'cust') {
792
- const desc = readVersionAndDescriptor(reader) as CustomDescriptor;
793
- // console.log('cust', target.name, require('util').inspect(desc, false, 99, true));
794
- if (desc.layerTime !== undefined) target.timestamp = desc.layerTime;
795
- } else if (key === 'mlst') {
796
- const desc = readVersionAndDescriptor(reader) as FrameListDescriptor;
797
- // console.log('mlst', target.name, require('util').inspect(desc, false, 99, true));
798
-
799
- target.animationFrames = [];
800
-
801
- for (let i = 0; i < desc.LaSt.length; i++) {
802
- const f = desc.LaSt[i];
803
- const frame: AnimationFrame = { frames: f.FrLs };
804
- if (f.enab !== undefined) frame.enable = f.enab;
805
- if (f.Ofst) frame.offset = horzVrtcToXY(f.Ofst);
806
- if (f.FXRf) frame.referencePoint = horzVrtcToXY(f.FXRf);
807
- if (f.Lefx) frame.effects = parseEffects(f.Lefx, !!reader.logMissingFeatures);
808
- if (f.blendOptions && f.blendOptions.Opct) frame.opacity = parsePercent(f.blendOptions.Opct);
809
- target.animationFrames.push(frame);
810
- }
811
- } else if (key === 'mdyn') {
812
- // frame flags
813
- readUint16(reader); // unknown
814
- const propagate = readUint8(reader);
815
- const flags = readUint8(reader);
816
-
817
- target.animationFrameFlags = {
818
- propagateFrameOne: !propagate,
819
- unifyLayerPosition: (flags & 1) !== 0,
820
- unifyLayerStyle: (flags & 2) !== 0,
821
- unifyLayerVisibility: (flags & 4) !== 0,
822
- };
823
- } else if (key === 'tmln') {
824
- const desc = readVersionAndDescriptor(reader) as TimelineDescriptor;
825
- const timeScope = desc.timeScope;
826
- // console.log('tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
827
-
828
- const timeline: Timeline = {
829
- start: frac(timeScope.Strt),
830
- duration: frac(timeScope.duration),
831
- inTime: frac(timeScope.inTime),
832
- outTime: frac(timeScope.outTime),
833
- autoScope: desc.autoScope,
834
- audioLevel: desc.audioLevel,
835
- };
1032
+ const end = reader.offset + 4 + reader.view.getUint32(reader.offset);
1033
+
1034
+ try {
1035
+ readSection(reader, 1, left => {
1036
+ if (key === 'cust') {
1037
+ const desc = readVersionAndDescriptor(reader) as CustomDescriptor;
1038
+ // console.log('cust', target.name, require('util').inspect(desc, false, 99, true));
1039
+ if (desc.layerTime !== undefined) target.timestamp = desc.layerTime;
1040
+ } else if (key === 'mlst') {
1041
+ const desc = readVersionAndDescriptor(reader) as FrameListDescriptor;
1042
+ // console.log('mlst', target.name, require('util').inspect(desc, false, 99, true));
1043
+
1044
+ target.animationFrames = [];
1045
+
1046
+ for (let i = 0; i < desc.LaSt.length; i++) {
1047
+ const f = desc.LaSt[i];
1048
+ const frame: AnimationFrame = { frames: f.FrLs };
1049
+ if (f.enab !== undefined) frame.enable = f.enab;
1050
+ if (f.Ofst) frame.offset = horzVrtcToXY(f.Ofst);
1051
+ if (f.FXRf) frame.referencePoint = horzVrtcToXY(f.FXRf);
1052
+ if (f.Lefx) frame.effects = parseEffects(f.Lefx, reader);
1053
+ if (f.blendOptions && f.blendOptions.Opct) frame.opacity = parsePercent(f.blendOptions.Opct);
1054
+ target.animationFrames.push(frame);
1055
+ }
1056
+ } else if (key === 'mdyn') {
1057
+ // frame flags
1058
+ readUint16(reader); // unknown
1059
+ const propagate = readUint8(reader);
1060
+ const flags = readUint8(reader);
1061
+
1062
+ target.animationFrameFlags = {
1063
+ propagateFrameOne: !propagate,
1064
+ unifyLayerPosition: (flags & 1) !== 0,
1065
+ unifyLayerStyle: (flags & 2) !== 0,
1066
+ unifyLayerVisibility: (flags & 4) !== 0,
1067
+ };
1068
+ } else if (key === 'tmln') {
1069
+ const desc = readVersionAndDescriptor(reader) as TimelineDescriptor;
1070
+ const timeScope = desc.timeScope;
1071
+ // console.log('tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
1072
+
1073
+ const timeline: Timeline = {
1074
+ start: frac(timeScope.Strt),
1075
+ duration: frac(timeScope.duration),
1076
+ inTime: frac(timeScope.inTime),
1077
+ outTime: frac(timeScope.outTime),
1078
+ autoScope: desc.autoScope,
1079
+ audioLevel: desc.audioLevel,
1080
+ };
836
1081
 
837
- if (desc.trackList) {
838
- timeline.tracks = parseTrackList(desc.trackList, !!reader.logMissingFeatures);
839
- }
1082
+ if (desc.trackList) {
1083
+ timeline.tracks = parseTrackList(desc.trackList, reader);
1084
+ }
840
1085
 
841
- target.timeline = timeline;
842
- // console.log('tmln:result', target.name, target.id, require('util').inspect(timeline, false, 99, true));
843
- } else if (key === 'cmls') {
844
- const desc = readVersionAndDescriptor(reader) as CmlsDescriptor;
845
- // console.log('cmls', require('util').inspect(desc, false, 99, true));
1086
+ target.timeline = timeline;
1087
+ // console.log('tmln:result', target.name, target.id, require('util').inspect(timeline, false, 99, true));
1088
+ } else if (key === 'cmls') {
1089
+ const desc = readVersionAndDescriptor(reader) as CmlsDescriptor;
1090
+ // console.log('cmls', require('util').inspect(desc, false, 99, true));
846
1091
 
847
- target.comps = {
848
- settings: [],
849
- };
1092
+ target.comps = {
1093
+ settings: [],
1094
+ };
850
1095
 
851
- if (desc.origFXRefPoint) target.comps.originalEffectsReferencePoint = { x: desc.origFXRefPoint.Hrzn, y: desc.origFXRefPoint.Vrtc };
1096
+ if (desc.origFXRefPoint) target.comps.originalEffectsReferencePoint = { x: desc.origFXRefPoint.Hrzn, y: desc.origFXRefPoint.Vrtc };
852
1097
 
853
- for (const item of desc.layerSettings) {
854
- target.comps.settings.push({ compList: item.compList });
855
- const t = target.comps.settings[target.comps.settings.length - 1];
856
- if ('enab' in item) t.enabled = item.enab;
857
- if (item.Ofst) t.offset = { x: item.Ofst.Hrzn, y: item.Ofst.Vrtc };
858
- if (item.FXRefPoint) t.effectsReferencePoint = { x: item.FXRefPoint.Hrzn, y: item.FXRefPoint.Vrtc };
859
- }
860
- } else if (key === 'extn') {
861
- interface ExtnDescriptor {
862
- generatorSettings: {
863
- exportAs: {
864
- exportOption: string;
865
- };
866
- layerTime: number;
867
- };
1098
+ for (const item of desc.layerSettings) {
1099
+ target.comps.settings.push({ compList: item.compList });
1100
+ const t = target.comps.settings[target.comps.settings.length - 1];
1101
+ if ('enab' in item) t.enabled = item.enab;
1102
+ if (item.Ofst) t.offset = { x: item.Ofst.Hrzn, y: item.Ofst.Vrtc };
1103
+ if (item.FXRefPoint) t.effectsReferencePoint = { x: item.FXRefPoint.Hrzn, y: item.FXRefPoint.Vrtc };
1104
+ }
1105
+ } else if (key === 'extn') {
1106
+ const desc = readVersionAndDescriptor(reader) as GeneratorSettingsDescriptor;
1107
+ if (desc.generatorSettings) target.generatorSettings = parseGeneratorSettings(desc.generatorSettings);
1108
+ } else {
1109
+ reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key);
868
1110
  }
869
1111
 
870
- const desc = readVersionAndDescriptor(reader) as ExtnDescriptor;
871
- // console.log(require('util').inspect(desc, false, 99, true));
872
- desc; // TODO: save this
873
- reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key);
874
- } else {
875
- reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key);
876
- }
877
-
878
- skipBytes(reader, left());
879
- });
1112
+ skipBytes(reader, left());
1113
+ });
1114
+ } catch (e) {
1115
+ // failure to read one item shouldn't drop all other items
1116
+ if (reader.throwForMissingFeatures) throw e;
1117
+ reader.logMissingFeatures && reader.log(`Failed to read "shmd" section key ${key}: ${(e as Error).message}`);
1118
+ reader.offset = end;
1119
+ }
880
1120
  }
881
1121
 
882
1122
  skipBytes(reader, left());
883
1123
  },
884
1124
  (writer, target, _, options) => {
885
1125
  const { animationFrames, animationFrameFlags, timestamp, timeline, comps } = target;
1126
+ const generatorSettings = isDocument(target) ? undefined : target.generatorSettings;
886
1127
 
887
1128
  let count = 0;
888
1129
  if (animationFrames) count++;
@@ -890,6 +1131,7 @@ addHandler(
890
1131
  if (timeline) count++;
891
1132
  if (timestamp !== undefined) count++;
892
1133
  if (comps) count++;
1134
+ if (generatorSettings) count++;
893
1135
  writeUint32(writer, count);
894
1136
 
895
1137
  if (animationFrames) {
@@ -910,8 +1152,8 @@ addHandler(
910
1152
  frame.FrLs = f.frames;
911
1153
  if (f.offset) frame.Ofst = xyToHorzVrtc(f.offset);
912
1154
  if (f.referencePoint) frame.FXRf = xyToHorzVrtc(f.referencePoint);
913
- if (f.effects) frame.Lefx = serializeEffects(f.effects, false, false);
914
- if (f.opacity !== undefined) frame.blendOptions = { Opct: unitsPercent(f.opacity) };
1155
+ if (f.effects) frame.Lefx = serializeEffects(f.effects, writer, hasMultiEffects(f.effects));
1156
+ if (f.opacity !== undefined) frame.blendOptions = { Opct: unitsPercentF(f.opacity) };
915
1157
  desc.LaSt.push(frame);
916
1158
  }
917
1159
 
@@ -954,7 +1196,7 @@ addHandler(
954
1196
  } as any;
955
1197
 
956
1198
  if (timeline.tracks) {
957
- desc.trackList = serializeTrackList(timeline.tracks);
1199
+ desc.trackList = serializeTrackList(timeline.tracks, writer);
958
1200
  }
959
1201
 
960
1202
  desc.LyrI = getLayerId(target, options);
@@ -1005,6 +1247,16 @@ addHandler(
1005
1247
  writeVersionAndDescriptor(writer, '', 'null', desc);
1006
1248
  }, true);
1007
1249
  }
1250
+
1251
+ if (generatorSettings) {
1252
+ writeSignature(writer, '8BIM');
1253
+ writeSignature(writer, 'extn');
1254
+ writeUint8(writer, 0); // copy (always false)
1255
+ writeZeros(writer, 3);
1256
+ writeSection(writer, 2, () => {
1257
+ writeVersionAndDescriptor(writer, '', 'null', serializeGeneratorSettings(generatorSettings));
1258
+ }, true);
1259
+ }
1008
1260
  },
1009
1261
  );
1010
1262
 
@@ -1096,7 +1348,7 @@ addHandler(
1096
1348
  lineAlignment: strokeStyleLineAlignment.decode(desc.strokeStyleLineAlignment),
1097
1349
  scaleLock: desc.strokeStyleScaleLock,
1098
1350
  strokeAdjust: desc.strokeStyleStrokeAdjust,
1099
- lineDashSet: desc.strokeStyleLineDashSet.map(parseUnits),
1351
+ lineDashSet: (desc.strokeStyleLineDashSet || []).map(parseUnits),
1100
1352
  blendMode: BlnM.decode(desc.strokeStyleBlendMode),
1101
1353
  opacity: parsePercent(desc.strokeStyleOpacity),
1102
1354
  content: parseVectorContent(desc.strokeStyleContent),
@@ -1135,8 +1387,8 @@ interface ArtbDescriptor {
1135
1387
  artboardRect: { 'Top ': number; Left: number; Btom: number; Rght: number; };
1136
1388
  guideIndeces: any[];
1137
1389
  artboardPresetName: string;
1138
- 'Clr ': DescriptorColor;
1139
- artboardBackgroundType: number;
1390
+ 'Clr '?: DescriptorColor;
1391
+ artboardBackgroundType?: number;
1140
1392
  }
1141
1393
 
1142
1394
  addHandler(
@@ -1149,10 +1401,11 @@ addHandler(
1149
1401
  rect: { top: rect['Top '], left: rect.Left, bottom: rect.Btom, right: rect.Rght },
1150
1402
  guideIndices: desc.guideIndeces,
1151
1403
  presetName: desc.artboardPresetName,
1152
- color: parseColor(desc['Clr ']),
1153
- backgroundType: desc.artboardBackgroundType,
1154
1404
  };
1155
1405
 
1406
+ if (desc['Clr ']) target.artboard.color = parseColor(desc['Clr ']);
1407
+ if (desc.artboardBackgroundType !== undefined) target.artboard.backgroundType = desc.artboardBackgroundType;
1408
+
1156
1409
  skipBytes(reader, left());
1157
1410
  },
1158
1411
  (writer, target) => {
@@ -1299,13 +1552,13 @@ function encodeWarp(warp: Warp): WarpDescriptor {
1299
1552
 
1300
1553
  addHandler(
1301
1554
  'PlLd',
1302
- hasKey('placedLayer'),
1555
+ target => !!target.placedLayer && !target.placedLayer.external, // linked smart objects are stored in SoLE
1303
1556
  (reader, target, left) => {
1304
1557
  if (readSignature(reader) !== 'plcL') throw new Error(`Invalid PlLd signature`);
1305
1558
  if (readInt32(reader) !== 3) throw new Error(`Invalid PlLd version`);
1306
1559
  const id = readPascalString(reader, 1);
1307
1560
  const pageNumber = readInt32(reader);
1308
- const totalPages = readInt32(reader); // TODO: check how this works ?
1561
+ const totalPages = readInt32(reader); // for multi-page documents (PDF)
1309
1562
  readInt32(reader); // anitAliasPolicy 16
1310
1563
  const placedLayerType = readInt32(reader); // 0 = unknown, 1 = vector, 2 = raster, 3 = image stack
1311
1564
  if (!placedLayerTypes[placedLayerType]) throw new Error('Invalid PlLd type');
@@ -1857,7 +2110,7 @@ type SoLdDescriptorFilterItem = {
1857
2110
  'Fl ': string;
1858
2111
  };
1859
2112
  } | {
1860
- filterID: 991 | 943; // TODO: why 2 different IDs? do we need to handle them separately?
2113
+ filterID: 991 | 943; // 943 in older Photoshop versions (2021), 991 in newer ones, settings are the same
1861
2114
  Fltr: {
1862
2115
  _name: 'Rigid Transform';
1863
2116
  _classID: 'rigidTransform';
@@ -2036,7 +2289,7 @@ type SoLdDescriptorFilterItem = {
2036
2289
  Fltr: {
2037
2290
  _name: 'Curves';
2038
2291
  _classID: 'Crvs';
2039
- presetKind: string; // 'presetKindType.presetKindCustom';
2292
+ presetKind?: string; // 'presetKindType.presetKindCustom';
2040
2293
  Adjs?: {
2041
2294
  _name: '';
2042
2295
  _classID: 'CrvA';
@@ -2045,6 +2298,14 @@ type SoLdDescriptorFilterItem = {
2045
2298
  Mpng?: number[];
2046
2299
  }[];
2047
2300
  };
2301
+ } | {
2302
+ filterID: 1282829427;
2303
+ Fltr: {
2304
+ _name: 'Levels';
2305
+ _classID: 'Lvls';
2306
+ presetKind?: string; // 'presetKindType.presetKindCustom';
2307
+ Adjs?: FilterLevelsAdjustment[];
2308
+ };
2048
2309
  } | {
2049
2310
  filterID: 1231976050;
2050
2311
  } | {
@@ -2066,6 +2327,18 @@ interface FilterCurvesCurvePoint {
2066
2327
  Cnty?: boolean;
2067
2328
  }
2068
2329
 
2330
+ interface FilterLevelsAdjustment {
2331
+ _name: '';
2332
+ _classID: 'LvlA';
2333
+ Chnl: string[]; // 'Chnl.Cmps' | 'Chnl.Rd ' | 'Chnl.Grn ' | 'Chnl.Bl '
2334
+ Inpt?: number[]; // shadow, highlight
2335
+ Otpt?: number[]; // shadow, highlight
2336
+ 'Gmm '?: number;
2337
+ }
2338
+
2339
+ const levelsFilterKeys = ['_name', '_classID', 'presetKind', 'Adjs'];
2340
+ const levelsFilterAdjustmentKeys = ['_name', '_classID', 'Chnl', 'Inpt', 'Otpt', 'Gmm '];
2341
+
2069
2342
  interface SoLdDescriptorFilter {
2070
2343
  _name: '',
2071
2344
  _classID: 'filterFXStyle',
@@ -2129,7 +2402,92 @@ function pointToHrznVrtc(point: { x: UnitsValue; y: UnitsValue; }): HrznVrtcDesc
2129
2402
  };
2130
2403
  }
2131
2404
 
2132
- function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): Filter | undefined {
2405
+ // Camera Raw Filter setting paths (in CameraRawFilterSettings) and their descriptor keys
2406
+ const cameraRawColorKeys: [keyof CameraRawColors, string][] = [
2407
+ ['red', 'R'], ['orange', 'O'], ['yellow', 'Y'], ['green', 'G'], ['aqua', 'A'], ['blue', 'B'], ['purple', 'P'], ['magenta', 'M'],
2408
+ ];
2409
+
2410
+ const cameraRawSettings: [string, string][] = [
2411
+ ['version', 'CrVe'], ['processVersion', 'PrVN'], ['processVersionCode', 'PrVe'], ['whiteBalance', 'WBal'],
2412
+ ['temperature', 'Temp'], ['tint', 'Tint'], ['exposure', 'Ex12'], ['contrast', 'Cr12'], ['highlights', 'Hi12'],
2413
+ ['shadows', 'Sh12'], ['whites', 'Wh12'], ['blacks', 'Bk12'], ['texture', 'CrTx'], ['clarity', 'Cl12'],
2414
+ ['dehaze', 'Dhze'], ['vibrance', 'Vibr'], ['saturation', 'Strt'],
2415
+ ['parametricCurve.shadows', 'PC_S'], ['parametricCurve.darks', 'PC_D'], ['parametricCurve.lights', 'PC_L'],
2416
+ ['parametricCurve.highlights', 'PC_H'], ['parametricCurve.shadowSplit', 'PC_1'], ['parametricCurve.midtoneSplit', 'PC_2'],
2417
+ ['parametricCurve.highlightSplit', 'PC_3'],
2418
+ ['pointCurve.composite', 'Crv '], ['pointCurve.red', 'CrvR'], ['pointCurve.green', 'CrvG'], ['pointCurve.blue', 'CrvB'],
2419
+ ...cameraRawColorKeys.map(([name, key]) => [`hsl.hue.${name}`, `HA_${key}`] as [string, string]),
2420
+ ...cameraRawColorKeys.map(([name, key]) => [`hsl.saturation.${name}`, `SA_${key}`] as [string, string]),
2421
+ ...cameraRawColorKeys.map(([name, key]) => [`hsl.luminance.${name}`, `LA_${key}`] as [string, string]),
2422
+ ['colorGrading.shadows.hue', 'STSH'], ['colorGrading.shadows.saturation', 'STSS'], ['colorGrading.shadows.luminance', 'CgSL'],
2423
+ ['colorGrading.midtones.hue', 'CgMH'], ['colorGrading.midtones.saturation', 'CgMS'], ['colorGrading.midtones.luminance', 'CgML'],
2424
+ ['colorGrading.highlights.hue', 'STHH'], ['colorGrading.highlights.saturation', 'STHS'], ['colorGrading.highlights.luminance', 'CgHL'],
2425
+ ['colorGrading.global.hue', 'CgGH'], ['colorGrading.global.saturation', 'CgGS'], ['colorGrading.global.luminance', 'CgGL'],
2426
+ ['colorGrading.blending', 'CgBl'], ['colorGrading.balance', 'STB '],
2427
+ ['sharpening.amount', 'Shrp'], ['sharpening.radius', 'ShpR'], ['sharpening.detail', 'ShpD'], ['sharpening.masking', 'ShpM'],
2428
+ ['noiseReduction.luminance', 'LNR '], ['noiseReduction.luminanceDetail', 'LNRD'], ['noiseReduction.luminanceContrast', 'LNRC'],
2429
+ ['noiseReduction.color', 'CNR '], ['noiseReduction.colorDetail', 'CNRD'], ['noiseReduction.colorSmoothness', 'CNRS'],
2430
+ ['grain.amount', 'GRNA'], ['grain.size', 'GRNS'], ['grain.roughness', 'GRNF'],
2431
+ ['postCropVignette.amount', 'PCVA'], ['postCropVignette.midpoint', 'PCVM'], ['postCropVignette.feather', 'PCVF'],
2432
+ ['postCropVignette.roundness', 'PCVR'],
2433
+ ];
2434
+
2435
+ const cameraRawCurveKeys = ['Crv ', 'CrvR', 'CrvG', 'CrvB'];
2436
+ const cameraRawFilterID = 2783;
2437
+
2438
+ function parseCameraRawSettings(desc: any, reader: PsdReader) {
2439
+ const settings: any = {};
2440
+
2441
+ for (const [path, key] of cameraRawSettings) {
2442
+ let value = desc[key];
2443
+ if (value === undefined) continue;
2444
+
2445
+ if (key === 'WBal') {
2446
+ value = (value as string).split('.')[1];
2447
+ } else if (cameraRawCurveKeys.includes(key)) {
2448
+ const values = value as number[];
2449
+ value = [];
2450
+ for (let i = 0; i + 1 < values.length; i += 2) value.push({ input: values[i], output: values[i + 1] });
2451
+ }
2452
+
2453
+ const parts = path.split('.');
2454
+ let target = settings;
2455
+ for (let i = 0; i < parts.length - 1; i++) target = target[parts[i]] = target[parts[i]] ?? {};
2456
+ target[parts[parts.length - 1]] = value;
2457
+ }
2458
+
2459
+ for (const key of Object.keys(desc)) {
2460
+ if (key !== '_name' && key !== '_classID' && !cameraRawSettings.some(s => s[1] === key)) {
2461
+ reader.logMissingFeatures && reader.log(`Unhandled Camera Raw Filter setting: ${key}`);
2462
+ }
2463
+ }
2464
+
2465
+ return settings as CameraRawFilterSettings;
2466
+ }
2467
+
2468
+ function serializeCameraRawSettings(settings: CameraRawFilterSettings) {
2469
+ const desc: any = { _name: '', _classID: cameraRawFilterClassID };
2470
+
2471
+ for (const [path, key] of cameraRawSettings) {
2472
+ let value: any = settings;
2473
+ for (const part of path.split('.')) value = value?.[part];
2474
+ if (value === undefined) continue;
2475
+
2476
+ if (key === 'WBal') {
2477
+ value = `WBal.${value}`;
2478
+ } else if (cameraRawCurveKeys.includes(key)) {
2479
+ value = (value as CameraRawCurvePoint[]).flatMap(p => [Math.round(p.input), Math.round(p.output)]);
2480
+ } else if (typeof value === 'number' && key !== 'Ex12' && key !== 'ShpR') {
2481
+ value = Math.round(value);
2482
+ }
2483
+
2484
+ desc[key] = value;
2485
+ }
2486
+
2487
+ return desc;
2488
+ }
2489
+
2490
+ function parseFilterFXItem(f: SoLdDescriptorFilterItem, reader: PsdReader): Filter | undefined {
2133
2491
  const base: Omit<Filter, 'type' | 'filter'> = {
2134
2492
  name: f['Nm '],
2135
2493
  opacity: parsePercent(f.blendOptions.Opct),
@@ -2140,6 +2498,10 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2140
2498
  backgroundColor: parseColor(f.BckC),
2141
2499
  };
2142
2500
 
2501
+ if ('Fltr' in f && (f.Fltr._classID as string) === cameraRawFilterClassID) {
2502
+ return { ...base, type: 'camera raw', filter: parseCameraRawSettings(f.Fltr, reader) };
2503
+ }
2504
+
2143
2505
  if ('Fltr' in f) {
2144
2506
  switch (f.Fltr._classID) {
2145
2507
  case 'boxblur': return {
@@ -2209,7 +2571,7 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2209
2571
  undefinedAreas: UndA.decode(f.Fltr.UndA),
2210
2572
  displacementFile: {
2211
2573
  signature: f.Fltr.DspF.sig,
2212
- path: f.Fltr.DspF.path, // TODO: this is decoded incorrectly ???
2574
+ path: f.Fltr.DspF.path,
2213
2575
  },
2214
2576
  },
2215
2577
  };
@@ -2544,8 +2906,7 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2544
2906
  ],
2545
2907
  puppetShapeList: (f.Fltr.puppetShapeList || []).map(p => ({
2546
2908
  rigidType: p.rigidType,
2547
- // TODO: VrsM
2548
- // TODO: VrsN
2909
+ // VrsM / VrsN - version (always 1.0)
2549
2910
  originalVertexArray: uint8ToPoints(p.originalVertexArray),
2550
2911
  deformedVertexArray: uint8ToPoints(p.deformedVertexArray),
2551
2912
  indexArray: Array.from(uint8ToUint32(p.indexArray)),
@@ -2664,7 +3025,8 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2664
3025
  ...base,
2665
3026
  type: 'curves',
2666
3027
  filter: {
2667
- presetKind: presetKindType.decode(f.Fltr.presetKind),
3028
+ // Photoshop doesn't write preset kind for filters with explicit values
3029
+ ...(f.Fltr.presetKind !== undefined ? { presetKind: presetKindType.decode(f.Fltr.presetKind) } : {}),
2668
3030
  ...(f.Fltr.Adjs ? {
2669
3031
  adjustments: f.Fltr.Adjs.map(a => {
2670
3032
  const channels = a.Chnl.map(Chnl.decode);
@@ -2687,6 +3049,40 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2687
3049
  },
2688
3050
  };
2689
3051
  };
3052
+ case 'Lvls': {
3053
+ const adjustments = f.Fltr.Adjs;
3054
+ const isKnown = (obj: object, keys: string[]) => Object.keys(obj).every(k => keys.indexOf(k) !== -1);
3055
+
3056
+ // skip filter with settings we don't know about
3057
+ if (!isKnown(f.Fltr, levelsFilterKeys) || (adjustments && !adjustments.every(a => isKnown(a, levelsFilterAdjustmentKeys)))) {
3058
+ if (reader.throwForMissingFeatures) throw new Error('Unknown levels filter settings');
3059
+ reader.logMissingFeatures && reader.log(`Skipped levels smart filter with unknown settings`);
3060
+ return undefined;
3061
+ }
3062
+
3063
+ return {
3064
+ ...base,
3065
+ type: 'levels',
3066
+ filter: {
3067
+ ...(f.Fltr.presetKind !== undefined ? { presetKind: presetKindType.decode(f.Fltr.presetKind) } : {}),
3068
+ ...(adjustments ? {
3069
+ adjustments: adjustments.map(a => {
3070
+ const result: LevelsFilterAdjustment = { channels: a.Chnl.map(Chnl.decode) };
3071
+ if (a.Inpt) {
3072
+ result.shadowInput = a.Inpt[0];
3073
+ result.highlightInput = a.Inpt[1];
3074
+ }
3075
+ if (a.Otpt) {
3076
+ result.shadowOutput = a.Otpt[0];
3077
+ result.highlightOutput = a.Otpt[1];
3078
+ }
3079
+ if (a['Gmm '] !== undefined) result.midtoneInput = a['Gmm '];
3080
+ return result;
3081
+ }),
3082
+ } : {}),
3083
+ },
3084
+ };
3085
+ };
2690
3086
  case 'BrgC': {
2691
3087
  return {
2692
3088
  ...base,
@@ -2699,10 +3095,11 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2699
3095
  };
2700
3096
  };
2701
3097
  default:
2702
- if (options.throwForMissingFeatures) {
3098
+ if (reader.throwForMissingFeatures) {
2703
3099
  // console.log('FILTER', require('util').inspect(f, false, 99, true));
2704
3100
  throw new Error(`Unknown filter classId: ${(f as any).Fltr._classID}`);
2705
3101
  }
3102
+ reader.logMissingFeatures && reader.log(`Unhandled smart filter: ${(f as any).Fltr._classID} (${f['Nm ']})`);
2706
3103
  return undefined;
2707
3104
  }
2708
3105
  } else {
@@ -2721,22 +3118,24 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2721
3118
  case 1314149187: return { ...base, type: 'ntsc colors' };
2722
3119
  case 1231976050: return { ...base, type: 'invert' }
2723
3120
  default:
2724
- if (options.throwForMissingFeatures) {
3121
+ if (reader.throwForMissingFeatures) {
2725
3122
  // console.log('FILTER', require('util').inspect(f, false, 99, true));
2726
3123
  throw new Error(`Unknown filterID: ${(f as any).filterID}`);
2727
3124
  }
3125
+ reader.logMissingFeatures && reader.log(`Unhandled smart filter: ${(f as any).filterID} (${f['Nm ']})`);
3126
+ return undefined;
2728
3127
  }
2729
3128
  }
2730
3129
  }
2731
3130
 
2732
- function parseFilterFX(desc: SoLdDescriptorFilter, options: ReadOptions): PlacedLayerFilter {
3131
+ function parseFilterFX(desc: SoLdDescriptorFilter, reader: PsdReader): PlacedLayerFilter {
2733
3132
  return {
2734
3133
  enabled: desc.enab,
2735
3134
  validAtPosition: desc.validAtPosition,
2736
3135
  maskEnabled: desc.filterMaskEnable,
2737
3136
  maskLinked: desc.filterMaskLinked,
2738
3137
  maskExtendWithWhite: desc.filterMaskExtendWithWhite,
2739
- list: desc.filterFXList.map(x => parseFilterFXItem(x, options)).filter((x): x is Filter => !!x),
3138
+ list: desc.filterFXList.map(x => parseFilterFXItem(x, reader)).filter((x): x is Filter => !!x),
2740
3139
  };
2741
3140
  }
2742
3141
 
@@ -3275,14 +3674,14 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3275
3674
  Fltr: {
3276
3675
  _name: 'Rigid Transform',
3277
3676
  _classID: 'rigidTransform',
3278
- 'null': ['Ordn.Trgt'], // TODO: ???
3677
+ 'null': ['Ordn.Trgt'], // target layer reference
3279
3678
  rigidType: f.filter.rigidType,
3280
3679
  puppetShapeList: (f.filter.puppetShapeList || []).map(p => ({
3281
3680
  _name: '',
3282
3681
  _classID: 'puppetShape',
3283
3682
  rigidType: p.rigidType,
3284
- VrsM: 1, // TODO: ???
3285
- VrsN: 0, // TODO: ???
3683
+ VrsM: 1, // version major
3684
+ VrsN: 0, // version minor
3286
3685
  originalVertexArray: toUint8(new Float32Array(pointsToArray(p.originalVertexArray))),
3287
3686
  deformedVertexArray: toUint8(new Float32Array(pointsToArray(p.deformedVertexArray))),
3288
3687
  indexArray: toUint8(new Uint32Array(p.indexArray)),
@@ -3397,7 +3796,7 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3397
3796
  Fltr: {
3398
3797
  _name: 'Curves',
3399
3798
  _classID: 'Crvs',
3400
- presetKind: presetKindType.encode(f.filter.presetKind),
3799
+ ...(f.filter.presetKind !== undefined ? { presetKind: presetKindType.encode(f.filter.presetKind) } : {}),
3401
3800
  ...(f.filter.adjustments ? {
3402
3801
  Adjs: f.filter.adjustments.map(a => 'curve' in a ? {
3403
3802
  _name: '',
@@ -3420,6 +3819,33 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3420
3819
  },
3421
3820
  filterID: 1131574899,
3422
3821
  };
3822
+ case 'levels': return {
3823
+ ...base,
3824
+ Fltr: {
3825
+ _name: 'Levels',
3826
+ _classID: 'Lvls',
3827
+ ...(f.filter.presetKind !== undefined ? { presetKind: presetKindType.encode(f.filter.presetKind) } : {}),
3828
+ ...(f.filter.adjustments ? {
3829
+ Adjs: f.filter.adjustments.map(a => {
3830
+ const adjustment: FilterLevelsAdjustment = { _name: '', _classID: 'LvlA', Chnl: a.channels.map(Chnl.encode) };
3831
+ if (a.shadowInput !== undefined || a.highlightInput !== undefined) {
3832
+ adjustment.Inpt = [Math.round(a.shadowInput ?? 0), Math.round(a.highlightInput ?? 255)];
3833
+ }
3834
+ if (a.shadowOutput !== undefined || a.highlightOutput !== undefined) {
3835
+ adjustment.Otpt = [Math.round(a.shadowOutput ?? 0), Math.round(a.highlightOutput ?? 255)];
3836
+ }
3837
+ if (a.midtoneInput !== undefined) adjustment['Gmm '] = a.midtoneInput;
3838
+ return adjustment;
3839
+ }),
3840
+ } : {}),
3841
+ },
3842
+ filterID: 1282829427,
3843
+ };
3844
+ case 'camera raw': return {
3845
+ ...base,
3846
+ Fltr: serializeCameraRawSettings(f.filter),
3847
+ filterID: cameraRawFilterID,
3848
+ } as any;
3423
3849
  case 'brightness/contrast': return {
3424
3850
  ...base,
3425
3851
  Fltr: {
@@ -3463,8 +3889,8 @@ interface SoLdDescriptor {
3463
3889
  'Sz ': { _name: '', _classID: 'Pnt ', Wdth: number; Hght: number; };
3464
3890
  Rslt: DescriptorUnitsValue;
3465
3891
  filterFX?: SoLdDescriptorFilter;
3466
- comp?: number; // TODO: support this ?
3467
- compInfo?: { compID: number; originalCompID: number; }; // TODO: support this ?
3892
+ comp?: number;
3893
+ compInfo?: { compID: number; originalCompID: number; };
3468
3894
  Impr?: {}; // ???
3469
3895
  ClMg?: { // TODO: support this ?
3470
3896
  _name: '';
@@ -3512,7 +3938,7 @@ function getWarpFromPlacedLayer(placed: PlacedLayer): Warp {
3512
3938
 
3513
3939
  addHandler(
3514
3940
  'SoLd',
3515
- hasKey('placedLayer'),
3941
+ target => !!target.placedLayer && !target.placedLayer.external, // linked smart objects are stored in SoLE
3516
3942
  (reader, target, left) => {
3517
3943
  if (readSignature(reader) !== 'soLD') throw new Error(`Invalid SoLd type`);
3518
3944
  const version = readInt32(reader);
@@ -3573,6 +3999,12 @@ addHandler(
3573
3999
  const placedType = placedLayerTypes.indexOf(placed.type);
3574
4000
  if (placedType === -1) throw new Error('Invalid placedLayer type');
3575
4001
 
4002
+ const warp = getWarpFromPlacedLayer(placed);
4003
+ // size of linked image matches warp bounds if it's not provided
4004
+ const bounds = warp.bounds;
4005
+ const width = placed.width || (bounds ? bounds.right.value - bounds.left.value : 0);
4006
+ const height = placed.height || (bounds ? bounds.bottom.value - bounds.top.value : 0);
4007
+
3576
4008
  const desc: SoLdDescriptor = {
3577
4009
  Idnt: placed.id,
3578
4010
  placed: placed.placed ?? placed.id,
@@ -3587,12 +4019,12 @@ addHandler(
3587
4019
  Trnf: placed.transform,
3588
4020
  nonAffineTransform: placed.nonAffineTransform ?? placed.transform,
3589
4021
  // quiltWarp: {} as any,
3590
- warp: encodeWarp(getWarpFromPlacedLayer(placed)),
4022
+ warp: encodeWarp(warp),
3591
4023
  'Sz ': {
3592
4024
  _name: '',
3593
4025
  _classID: 'Pnt ',
3594
- Wdth: placed.width || 0, // TODO: find size ?
3595
- Hght: placed.height || 0, // TODO: find size ?
4026
+ Wdth: width,
4027
+ Hght: height,
3596
4028
  },
3597
4029
  Rslt: placed.resolution ? unitsValue(placed.resolution, 'resolution') : { units: 'Density', value: 72 },
3598
4030
  };
@@ -3610,15 +4042,6 @@ addHandler(
3610
4042
  };
3611
4043
  }
3612
4044
 
3613
- // TODO:
3614
- // desc.comp = -1;
3615
- // desc.compInfo = { _name: '', _classID: 'null', compID: -1, originalCompID: -1 } as any;
3616
- // desc.ClMg = {
3617
- // _name: '',
3618
- // _classID: 'ClMg',
3619
- // placedLayerOCIOConversion: 'placedLayerOCIOConversion.placedLayerOCIOConvertEmbedded'
3620
- // } as any;
3621
-
3622
4045
  // if (JSON.stringify(t) !== JSON.stringify(desc)) {
3623
4046
  // console.log('read', require('util').inspect(t, false, 99, true));
3624
4047
  // console.log('write', require('util').inspect(desc, false, 99, true));
@@ -3650,7 +4073,17 @@ addHandler(
3650
4073
  },
3651
4074
  );
3652
4075
 
3653
- addHandlerAlias('SoLE', 'SoLd');
4076
+ const soLdHandler = infoHandlersMap['SoLd'];
4077
+
4078
+ addHandler(
4079
+ 'SoLE', // linked smart object (Photoshop doesn't write PlLd for these)
4080
+ target => !!target.placedLayer?.external,
4081
+ (reader, target, left, psd, imageResources) => {
4082
+ soLdHandler.read(reader, target, left, psd, imageResources);
4083
+ target.placedLayer!.external = true;
4084
+ },
4085
+ soLdHandler.write,
4086
+ );
3654
4087
 
3655
4088
  addHandler(
3656
4089
  'fxrp',
@@ -3667,25 +4100,50 @@ addHandler(
3667
4100
  },
3668
4101
  );
3669
4102
 
3670
- addHandler(
3671
- 'Lr16',
3672
- () => false,
3673
- (reader, _target, _left, psd, imageResources) => {
3674
- readLayerInfo(reader, psd, imageResources);
3675
- },
3676
- (_writer, _target) => {
3677
- },
3678
- );
4103
+ function createGlobalLayerInfoHandler(key: string, bitsPerChannel: number | undefined) {
4104
+ addHandler(
4105
+ key,
4106
+ () => false,
4107
+ (reader, _target, left, psd, imageResources) => {
4108
+ // Photoshop writes empty (or 8bit compatibility) layer info section alongside Lr16/Lr32 in 16/32bit documents,
4109
+ // layers from global info with matching bit depth replace layers from that section, other ones are only used
4110
+ // if the section is empty
4111
+ if (!reader.layersInGlobalInfo && (psd.bitsPerChannel === bitsPerChannel || !psd.children?.length)) {
4112
+ const sectionLayers = psd.children;
4113
+ psd.children = [];
4114
+
4115
+ try {
4116
+ readLayerInfo(reader, psd, imageResources);
4117
+ } catch (e) {
4118
+ psd.children = sectionLayers; // keep layers from layer info section if global info is broken
4119
+ throw e;
4120
+ }
3679
4121
 
3680
- addHandler(
3681
- 'Lr32',
3682
- () => false,
3683
- (reader, _target, _left, psd, imageResources) => {
3684
- readLayerInfo(reader, psd, imageResources);
3685
- },
3686
- (_writer, _target) => {
3687
- },
3688
- );
4122
+ reader.layersInGlobalInfo = true;
4123
+ }
4124
+
4125
+ skipBytes(reader, left());
4126
+ },
4127
+ () => { },
4128
+ );
4129
+ }
4130
+
4131
+ createGlobalLayerInfoHandler('Lr16', 16);
4132
+ createGlobalLayerInfoHandler('Lr32', 32);
4133
+ createGlobalLayerInfoHandler('Layr', undefined);
4134
+
4135
+ // merged transparency markers, first extra channel of composite image is transparency
4136
+ for (const key of ['Mtrn', 'Mt16', 'Mt32']) {
4137
+ addHandler(
4138
+ key,
4139
+ () => false,
4140
+ (reader, _target, left) => {
4141
+ reader.globalAlpha = true;
4142
+ skipBytes(reader, left());
4143
+ },
4144
+ () => { },
4145
+ );
4146
+ }
3689
4147
 
3690
4148
  addHandler(
3691
4149
  'LMsk',
@@ -3708,35 +4166,24 @@ addHandler(
3708
4166
  },
3709
4167
  );
3710
4168
 
3711
- if (MOCK_HANDLERS) {
3712
- addHandler(
3713
- 'Patt',
3714
- target => (target as any)._Patt !== undefined,
3715
- (reader, target, left) => {
3716
- // console.log('additional info: Patt');
3717
- (target as any)._Patt = readBytes(reader, left());
3718
- },
3719
- (writer, target) => false && writeBytes(writer, (target as any)._Patt),
3720
- );
3721
- } else {
3722
- addHandler(
3723
- 'Patt', // TODO: handle also Pat2 & Pat3
3724
- target => !!(target.patterns && target.patterns.length > 0),
3725
- (reader, target, left) => {
3726
- while (left() > 0) {
3727
- const pattern = readPattern(reader);
3728
- if (target.patterns === undefined) target.patterns = [];
3729
- target.patterns.push(pattern);
3730
- }
3731
- },
3732
- (writer, target, _, _options) => {
3733
- const patterns = target.patterns || [];
3734
- for (const pattern of patterns) {
3735
- writePattern(writer, pattern);
3736
- }
3737
- },
3738
- );
3739
- }
4169
+ addHandler(
4170
+ 'Patt', // 'Pat2' and 'Pat3' (16 and 32 bit documents) are aliases, patterns are always written as 8 bit
4171
+ target => !!(target.patterns && target.patterns.length > 0),
4172
+ (reader, target, left) => {
4173
+ while (left() > 0) {
4174
+ const pattern = readPatternSafe(reader);
4175
+ if (!pattern) continue;
4176
+ if (target.patterns === undefined) target.patterns = [];
4177
+ target.patterns.push(pattern);
4178
+ }
4179
+ },
4180
+ (writer, target, _, _options) => {
4181
+ const patterns = target.patterns || [];
4182
+ for (const pattern of patterns) {
4183
+ writePattern(writer, pattern);
4184
+ }
4185
+ },
4186
+ );
3740
4187
 
3741
4188
  addHandlerAlias('Pat2', 'Patt');
3742
4189
  addHandlerAlias('Pat3', 'Patt');
@@ -3954,6 +4401,11 @@ interface LinkedFileDescriptor {
3954
4401
  relPath: string;
3955
4402
  }
3956
4403
 
4404
+ function readFileTypeSignature(reader: PsdReader) {
4405
+ const value = readSignature(reader);
4406
+ return /^[\0 ]*$/.test(value) ? '' : value.trim(); // empty value is stored as spaces or zeros
4407
+ }
4408
+
3957
4409
  function createLnkHandler(tag: string) {
3958
4410
  addHandler(
3959
4411
  tag,
@@ -3978,8 +4430,8 @@ function createLnkHandler(tag: string) {
3978
4430
  const id = readPascalString(reader, 1);
3979
4431
  const name = readUnicodeString(reader);
3980
4432
 
3981
- const fileType = readSignature(reader).trim(); // ' ' if empty
3982
- const fileCreator = readSignature(reader).trim(); // ' ' or '\0\0\0\0' if empty
4433
+ const fileType = readFileTypeSignature(reader); // ' ' if empty
4434
+ const fileCreator = readFileTypeSignature(reader); // ' ' or '\0\0\0\0' if empty
3983
4435
  const dataSize = readLength64(reader);
3984
4436
  const hasFileOpenDescriptor = readUint8(reader);
3985
4437
  const fileOpenDescriptor = hasFileOpenDescriptor ? readVersionAndDescriptor(reader) as FileOpenDescriptor : undefined;
@@ -4137,28 +4589,55 @@ addHandlerAlias('lnk3', 'lnk2');
4137
4589
 
4138
4590
  interface PthsDescriptor {
4139
4591
  pathList: {
4592
+ _name: '';
4140
4593
  _classID: 'pathInfoClass';
4141
4594
  pathUnicodeName: string;
4142
- pathSymmetryClass: {
4595
+ pathSymmetryClass?: {
4596
+ _name: '';
4143
4597
  _classID: 'pathSymmetryClass';
4144
4598
  pathSymmetryMode: string; // 'pathSymmetryModeEnum.pathSymmetryModeBasicPath'
4145
4599
  };
4146
4600
  }[];
4147
4601
  }
4148
4602
 
4603
+ // unicode names of saved paths (path data is stored in image resources 2000-2998)
4149
4604
  addHandler(
4150
4605
  'pths',
4151
- hasKey('pathList'),
4152
- (reader, target) => {
4606
+ target => (target as Psd).imageResources?.savedPaths !== undefined,
4607
+ (reader, _target, left, psd) => {
4153
4608
  const desc = readVersionAndDescriptor(reader, true) as PthsDescriptor;
4154
- // console.log(require('util').inspect(desc, false, 99, true));
4155
- // if (options.throwForMissingFeatures && desc?.pathList?.length) throw new Error('non-empty pathList in `pths`');
4156
- desc;
4157
- target.pathList = []; // TODO: read paths
4609
+ const imageResources = psd.imageResources ??= {};
4610
+ const savedPaths = imageResources.savedPaths ??= [];
4611
+
4612
+ for (let i = 0; i < desc.pathList.length && i < savedPaths.length; i++) {
4613
+ const { pathUnicodeName, pathSymmetryClass } = desc.pathList[i];
4614
+ const path = savedPaths[i];
4615
+
4616
+ // clipping path is referenced by (non-unicode) name
4617
+ if (imageResources.clippingPath && imageResources.clippingPath.name === path.name) {
4618
+ imageResources.clippingPath.name = pathUnicodeName;
4619
+ }
4620
+
4621
+ path.name = pathUnicodeName;
4622
+ if (pathSymmetryClass) path.symmetryMode = pathSymmetryClass.pathSymmetryMode.split('.')[1];
4623
+ }
4624
+
4625
+ skipBytes(reader, left());
4158
4626
  },
4159
- (writer, _target) => {
4627
+ (writer, _target, psd) => {
4160
4628
  const desc: PthsDescriptor = {
4161
- pathList: [], // TODO: write paths
4629
+ pathList: psd.imageResources!.savedPaths!.map(path => ({
4630
+ _name: '',
4631
+ _classID: 'pathInfoClass',
4632
+ pathUnicodeName: path.name,
4633
+ ...(path.symmetryMode ? {
4634
+ pathSymmetryClass: {
4635
+ _name: '',
4636
+ _classID: 'pathSymmetryClass',
4637
+ pathSymmetryMode: `pathSymmetryModeEnum.${path.symmetryMode}`,
4638
+ },
4639
+ } : {}),
4640
+ })),
4162
4641
  };
4163
4642
 
4164
4643
  writeVersionAndDescriptor(writer, '', 'pathsDataClass', desc);
@@ -4173,26 +4652,19 @@ addHandler(
4173
4652
  );
4174
4653
 
4175
4654
  addHandler(
4176
- 'lfxs',
4177
- () => false, // TODO: not sure when we actually need to write this section
4178
- // NOTE: this might be insufficient
4179
- // target => target.effects !== undefined && (
4180
- // !!target.effects.dropShadow?.some(e => e.choke) ||
4181
- // !!target.effects.innerShadow?.some(e => e.choke) ||
4182
- // !!target.effects.outerGlow?.choke ||
4183
- // !!target.effects.innerGlow?.choke
4184
- // ),
4655
+ 'lfxs', // effects of group layers (instead of 'lfx2' / 'lmfx')
4656
+ target => target.effects !== undefined && isGroupLayer(target),
4185
4657
  (reader, target, left) => {
4186
4658
  const version = readUint32(reader);
4187
4659
  if (version !== 0) throw new Error(`Invalid lfxs version`);
4188
4660
 
4189
4661
  const desc: Lfx2Descriptor & LmfxDescriptor = readVersionAndDescriptor(reader);
4190
- target.effects = parseEffects(desc, !!reader.logMissingFeatures);
4662
+ target.effects = parseEffects(desc, reader);
4191
4663
 
4192
4664
  skipBytes(reader, left());
4193
4665
  },
4194
- (writer, target, _, options) => {
4195
- const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true);
4666
+ (writer, target) => {
4667
+ const desc = serializeEffects(target.effects!, writer, true);
4196
4668
  writeUint32(writer, 0); // version
4197
4669
  writeVersionAndDescriptor(writer, '', 'null', desc);
4198
4670
  },
@@ -4238,6 +4710,12 @@ function readLevelsChannel(reader: PsdReader): LevelsAdjustmentChannel {
4238
4710
  return { shadowInput, highlightInput, shadowOutput, highlightOutput, midtoneInput };
4239
4711
  }
4240
4712
 
4713
+ const defaultLevelsChannel: LevelsAdjustmentChannel = { shadowInput: 0, highlightInput: 255, shadowOutput: 0, highlightOutput: 255, midtoneInput: 1 };
4714
+
4715
+ function isDefaultLevelsChannel(c: LevelsAdjustmentChannel) {
4716
+ return c.shadowInput === 0 && c.highlightInput === 255 && c.shadowOutput === 0 && c.highlightOutput === 255 && c.midtoneInput === 1;
4717
+ }
4718
+
4241
4719
  function writeLevelsChannel(writer: PsdWriter, channel: LevelsAdjustmentChannel) {
4242
4720
  writeInt16(writer, channel.shadowInput);
4243
4721
  writeInt16(writer, channel.highlightInput);
@@ -4252,62 +4730,56 @@ addHandler(
4252
4730
  (reader, target, left) => {
4253
4731
  if (readUint16(reader) !== 2) throw new Error('Invalid levl version');
4254
4732
 
4255
- const levels: LevelsAdjustmentChannel[] = [];
4733
+ const channels: LevelsAdjustmentChannel[] = [];
4734
+
4735
+ for (let i = 0; i < 29 && left() >= 10; i++) {
4736
+ channels.push(readLevelsChannel(reader));
4737
+ }
4738
+
4739
+ if (left() >= 8) {
4740
+ const signature = readSignature(reader);
4741
+ let totalChannelRecords: number;
4256
4742
 
4257
- for (let i = 0; i < 29 && left(); i++) {
4258
- levels.push(readLevelsChannel(reader));
4743
+ if (signature === 'ls\x00\x03') {
4744
+ // Clip Studio Paint writes 'ls' instead of 'Lvls' signature (version is part of the read signature)
4745
+ totalChannelRecords = readUint16(reader);
4746
+ } else {
4747
+ if (signature !== 'Lvls') throw new Error(`Invalid levl signature`);
4748
+ if (readUint16(reader) !== 3) throw new Error(`Invalid levl version`);
4749
+ totalChannelRecords = readUint16(reader);
4750
+ }
4751
+
4752
+ while (channels.length < totalChannelRecords && left() >= 10) {
4753
+ channels.push(readLevelsChannel(reader));
4754
+ }
4259
4755
  }
4260
4756
 
4261
- const [rgb, red, green, blue] = levels;
4757
+ // records for all possible channels are stored, skip unused ones
4758
+ while (channels.length && isDefaultLevelsChannel(channels[channels.length - 1])) channels.pop();
4262
4759
 
4263
4760
  target.adjustment = {
4264
4761
  ...target.adjustment as PresetInfo,
4265
4762
  type: 'levels',
4266
- rgb,
4267
- red,
4268
- green,
4269
- blue,
4763
+ channels,
4270
4764
  };
4271
4765
 
4272
- if (left()) {
4273
- if (readSignature(reader) !== 'Lvls') throw new Error(`Invalid levl signature`);
4274
- if (readUint16(reader) !== 3) throw new Error(`Invalid levl version`);
4275
-
4276
- const totalChannelRecords = readUint16(reader);
4277
-
4278
- for (let i = 0; i < (totalChannelRecords - levels.length) && left(); i++) {
4279
- levels.push(readLevelsChannel(reader));
4280
- }
4281
- }
4282
-
4283
4766
  skipBytes(reader, left());
4284
4767
  },
4285
4768
  (writer, target) => {
4286
- const info = target.adjustment as LevelsAdjustment;
4287
- const defaultChannel = {
4288
- shadowInput: 0,
4289
- highlightInput: 255,
4290
- shadowOutput: 0,
4291
- highlightOutput: 255,
4292
- midtoneInput: 1,
4293
- };
4769
+ const channels = (target.adjustment as LevelsAdjustment).channels || [];
4294
4770
 
4295
4771
  writeUint16(writer, 2); // version
4296
- writeLevelsChannel(writer, info.rgb || defaultChannel);
4297
- writeLevelsChannel(writer, info.red || defaultChannel);
4298
- writeLevelsChannel(writer, info.green || defaultChannel);
4299
- writeLevelsChannel(writer, info.blue || defaultChannel);
4300
4772
 
4301
- for (let i = 0; i < (29 - 4); i++) {
4302
- writeLevelsChannel(writer, defaultChannel);
4773
+ for (let i = 0; i < 29; i++) {
4774
+ writeLevelsChannel(writer, channels[i] || defaultLevelsChannel);
4303
4775
  }
4304
4776
 
4305
4777
  writeSignature(writer, 'Lvls');
4306
4778
  writeUint16(writer, 3); // version
4307
4779
  writeUint16(writer, 62); // total count
4308
4780
 
4309
- for (let i = 0; i < (62 - 29); i++) {
4310
- writeLevelsChannel(writer, defaultChannel);
4781
+ for (let i = 29; i < 62; i++) {
4782
+ writeLevelsChannel(writer, channels[i] || defaultLevelsChannel);
4311
4783
  }
4312
4784
  },
4313
4785
  );
@@ -4338,71 +4810,82 @@ addHandler(
4338
4810
  'curv',
4339
4811
  adjustmentType('curves'),
4340
4812
  (reader, target, left) => {
4341
- readUint8(reader);
4342
- if (readUint16(reader) !== 1) throw new Error('Invalid curv version');
4343
- readUint16(reader);
4344
- const channels = readUint16(reader);
4345
- const info: CurvesAdjustment = { type: 'curves' };
4813
+ const isMap = !!readUint8(reader); // curves drawn with pencil tool (lookup tables instead of points)
4814
+ const version = readUint16(reader);
4815
+ if (version !== 1 && version !== 4) throw new Error('Invalid curv version');
4816
+ const countOrMask = readUint32(reader);
4817
+ const channels: (CurvesAdjustmentChannel | number[])[] = [];
4818
+ const readChannel = () => isMap ? Array.from(readBytes(reader, 256)) : readCurveChannel(reader);
4819
+
4820
+ if (version === 1) {
4821
+ for (let i = 0; i < 32; i++) {
4822
+ if (countOrMask & (1 << i)) {
4823
+ while (channels.length < i) channels.push([]);
4824
+ channels.push(readChannel());
4825
+ }
4826
+ }
4827
+ } else {
4828
+ if (countOrMask > 64) throw new Error('Invalid curv channel count');
4829
+ for (let i = 0; i < countOrMask; i++) channels.push(readChannel());
4830
+ }
4346
4831
 
4347
- if (channels & 1) info.rgb = readCurveChannel(reader);
4348
- if (channels & 2) info.red = readCurveChannel(reader);
4349
- if (channels & 4) info.green = readCurveChannel(reader);
4350
- if (channels & 8) info.blue = readCurveChannel(reader);
4832
+ // extra section stores curves for all channels (including ones that don't fit in the mask)
4833
+ if (version === 1 && left() >= 10 && readSignature(reader) === 'Crv ') {
4834
+ readUint16(reader); // version
4835
+ const count = readUint32(reader);
4836
+
4837
+ for (let i = 0; i < count && left() >= 2; i++) {
4838
+ const index = readUint16(reader);
4839
+ const channel = readChannel();
4840
+ while (channels.length < index) channels.push([]);
4841
+ channels[index] = channel;
4842
+ }
4843
+ }
4351
4844
 
4352
4845
  target.adjustment = {
4353
4846
  ...target.adjustment as PresetInfo,
4354
- ...info,
4847
+ type: 'curves',
4848
+ ...(isMap ? { values: channels as number[][] } : { channels: channels as CurvesAdjustmentChannel[] }),
4355
4849
  };
4356
4850
 
4357
- // ignoring, duplicate information
4358
- // checkSignature(reader, 'Crv ');
4359
-
4360
- // const cVersion = readUint16(reader);
4361
- // readUint16(reader);
4362
- // const channelCount = readUint16(reader);
4363
-
4364
- // for (let i = 0; i < channelCount; i++) {
4365
- // const index = readUint16(reader);
4366
- // const nodes = readUint16(reader);
4367
-
4368
- // for (let j = 0; j < nodes; j++) {
4369
- // const output = readInt16(reader);
4370
- // const input = readInt16(reader);
4371
- // }
4372
- // }
4373
-
4374
4851
  skipBytes(reader, left());
4375
4852
  },
4376
4853
  (writer, target) => {
4377
- const info = target.adjustment as CurvesAdjustment;
4378
- const { rgb, red, green, blue } = info;
4379
- let channels = 0;
4380
- let channelCount = 0;
4854
+ const adjustment = target.adjustment as CurvesAdjustment;
4855
+ const isMap = !!adjustment.values;
4856
+ const channels: (CurvesAdjustmentChannel | number[])[] = (isMap ? adjustment.values : adjustment.channels) || [];
4857
+ const used: number[] = [];
4858
+ let channelsMask = 0;
4859
+
4860
+ for (let i = 0; i < channels.length; i++) {
4861
+ if (channels[i]?.length) {
4862
+ if (i < 32) channelsMask |= 1 << i;
4863
+ used.push(i);
4864
+ }
4865
+ }
4381
4866
 
4382
- if (rgb && rgb.length) { channels |= 1; channelCount++; }
4383
- if (red && red.length) { channels |= 2; channelCount++; }
4384
- if (green && green.length) { channels |= 4; channelCount++; }
4385
- if (blue && blue.length) { channels |= 8; channelCount++; }
4867
+ const writeChannel = (channel: CurvesAdjustmentChannel | number[]) => {
4868
+ if (isMap) {
4869
+ for (let i = 0; i < 256; i++) writeUint8(writer, clamp(Math.round((channel as number[])[i] ?? i), 0, 255));
4870
+ } else {
4871
+ writeCurveChannel(writer, channel as CurvesAdjustmentChannel);
4872
+ }
4873
+ };
4386
4874
 
4387
- writeUint8(writer, 0);
4875
+ writeUint8(writer, isMap ? 1 : 0);
4388
4876
  writeUint16(writer, 1); // version
4389
- writeUint16(writer, 0);
4390
- writeUint16(writer, channels);
4877
+ writeUint32(writer, channelsMask >>> 0);
4391
4878
 
4392
- if (rgb && rgb.length) writeCurveChannel(writer, rgb);
4393
- if (red && red.length) writeCurveChannel(writer, red);
4394
- if (green && green.length) writeCurveChannel(writer, green);
4395
- if (blue && blue.length) writeCurveChannel(writer, blue);
4879
+ for (const i of used) if (i < 32) writeChannel(channels[i]);
4396
4880
 
4397
4881
  writeSignature(writer, 'Crv ');
4398
4882
  writeUint16(writer, 4); // version
4399
- writeUint16(writer, 0);
4400
- writeUint16(writer, channelCount);
4883
+ writeUint32(writer, used.length);
4401
4884
 
4402
- if (rgb && rgb.length) { writeUint16(writer, 0); writeCurveChannel(writer, rgb); }
4403
- if (red && red.length) { writeUint16(writer, 1); writeCurveChannel(writer, red); }
4404
- if (green && green.length) { writeUint16(writer, 2); writeCurveChannel(writer, green); }
4405
- if (blue && blue.length) { writeUint16(writer, 3); writeCurveChannel(writer, blue); }
4885
+ for (const i of used) {
4886
+ writeUint16(writer, i);
4887
+ writeChannel(channels[i]);
4888
+ }
4406
4889
  },
4407
4890
  );
4408
4891
 
@@ -4487,10 +4970,26 @@ addHandler(
4487
4970
  (reader, target, left) => {
4488
4971
  if (readUint16(reader) !== 2) throw new Error('Invalid hue2 version');
4489
4972
 
4973
+ const colorize = !!readUint8(reader);
4974
+ skipBytes(reader, 1);
4975
+ const colorizeHue = readInt16(reader); // stored as -180..180
4976
+ const colorizeSaturation = readInt16(reader);
4977
+ const colorizeLightness = readInt16(reader);
4978
+
4490
4979
  target.adjustment = {
4491
4980
  ...target.adjustment as PresetInfo,
4492
4981
  type: 'hue/saturation',
4493
- master: readHueChannel(reader),
4982
+ colorize,
4983
+ colorization: {
4984
+ hue: colorizeHue < 0 ? colorizeHue + 360 : colorizeHue,
4985
+ saturation: colorizeSaturation,
4986
+ lightness: colorizeLightness,
4987
+ },
4988
+ master: {
4989
+ hue: readInt16(reader),
4990
+ saturation: readInt16(reader),
4991
+ lightness: readInt16(reader),
4992
+ },
4494
4993
  reds: readHueChannel(reader),
4495
4994
  yellows: readHueChannel(reader),
4496
4995
  greens: readHueChannel(reader),
@@ -4505,7 +5004,15 @@ addHandler(
4505
5004
  const info = target.adjustment as HueSaturationAdjustment;
4506
5005
 
4507
5006
  writeUint16(writer, 2); // version
4508
- writeHueChannel(writer, info.master);
5007
+ writeUint8(writer, info.colorize ? 1 : 0);
5008
+ writeUint8(writer, 0);
5009
+ const colorizeHue = Math.round(info.colorization?.hue ?? 0);
5010
+ writeInt16(writer, colorizeHue > 180 ? colorizeHue - 360 : colorizeHue);
5011
+ writeInt16(writer, Math.round(info.colorization?.saturation ?? 0));
5012
+ writeInt16(writer, Math.round(info.colorization?.lightness ?? 0));
5013
+ writeInt16(writer, Math.round(info.master?.hue ?? 0));
5014
+ writeInt16(writer, Math.round(info.master?.saturation ?? 0));
5015
+ writeInt16(writer, Math.round(info.master?.lightness ?? 0));
4509
5016
  writeHueChannel(writer, info.reds);
4510
5017
  writeHueChannel(writer, info.yellows);
4511
5018
  writeHueChannel(writer, info.greens);
@@ -4619,16 +5126,17 @@ addHandler(
4619
5126
  if (version === 2) {
4620
5127
  color = readColor(reader);
4621
5128
  } else { // version 3
4622
- // TODO: test this, this is probably wrong
4623
- // values are stored as L (0..100), a, b (-128..127) multiplied by 100,
4624
- // normalize them to the same scale as readColor() so they can be written back as version 2
4625
- const l = readInt32(reader) / 100;
4626
- const a = readInt32(reader) / 100;
4627
- const b = readInt32(reader) / 100;
5129
+ // XYZ color relative to D50 white point, 32768 = 1.0 (not documented, matches colors of Photoshop presets),
5130
+ // converted to Lab (same scale as readColor()) so it can be written back as version 2
5131
+ const x = readInt32(reader) / 32768;
5132
+ const y = readInt32(reader) / 32768;
5133
+ const z = readInt32(reader) / 32768;
5134
+ if (!(x >= 0 && y >= 0 && z >= 0 && x <= 2 && y <= 2 && z <= 2)) throw new Error('Invalid phfl XYZ color');
5135
+ const f = (t: number) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
4628
5136
  color = {
4629
- l: l / 100,
4630
- a: normalizeLabAB(a),
4631
- b: normalizeLabAB(b),
5137
+ l: Math.max(0, 116 * f(y) - 16) / 100,
5138
+ a: normalizeLabAB(500 * (f(x) - f(y))),
5139
+ b: normalizeLabAB(200 * (f(y) - f(z))),
4632
5140
  };
4633
5141
  }
4634
5142
 
@@ -4844,7 +5352,7 @@ addHandler(
4844
5352
  const hasMethod = !!readUint8(reader); reader.offset--;
4845
5353
  if (hasMethod) {
4846
5354
  const method = readSignature(reader);
4847
- info.method = gradientInterpolationMethodType.decode(method);
5355
+ info.method = gradientInterpolationMethodType.decode(`gradientInterpolationMethodType.${method}`);
4848
5356
  }
4849
5357
 
4850
5358
  info.name = readUnicodeString(reader);
@@ -4917,7 +5425,7 @@ addHandler(
4917
5425
  writeUint8(writer, info.dither ? 1 : 0);
4918
5426
 
4919
5427
  if (info.method !== undefined) {
4920
- writeSignature(writer, gradientInterpolationMethodType.encode(info.method));
5428
+ writeSignature(writer, gradientInterpolationMethodType.encode(info.method).split('.')[1]);
4921
5429
  }
4922
5430
 
4923
5431
  writeUnicodeStringWithPadding(writer, info.name || '');
@@ -5316,6 +5824,10 @@ addHandler(
5316
5824
  );
5317
5825
 
5318
5826
  addHandlerAlias('FXid', 'FEid');
5827
+ addHandlerAlias('FELS', 'FEid'); // filter effects 3
5828
+ addHandlerAlias('hue ', 'hue2'); // old hue/saturation (Photoshop 4.0), same structure as 'hue2'
5829
+ addHandlerAlias('plLd', 'PlLd'); // old placed layer key
5830
+ addHandlerAlias('abdd', 'artb'); // artboard data 3
5319
5831
 
5320
5832
  addHandler(
5321
5833
  'FMsk',
@@ -5334,14 +5846,14 @@ addHandler(
5334
5846
 
5335
5847
  interface ArtdDescriptor {
5336
5848
  'Cnt ': number;
5337
- autoExpandOffset: { Hrzn: number; Vrtc: number; };
5338
- origin: { Hrzn: number; Vrtc: number; };
5339
- autoExpandEnabled: boolean;
5340
- autoNestEnabled: boolean;
5341
- autoPositionEnabled: boolean;
5849
+ autoExpandOffset?: { Hrzn: number; Vrtc: number; };
5850
+ origin?: { Hrzn: number; Vrtc: number; };
5851
+ autoExpandEnabled?: boolean;
5852
+ autoNestEnabled?: boolean;
5853
+ autoPositionEnabled?: boolean;
5342
5854
  shrinkwrapOnSaveEnabled?: boolean;
5343
- docDefaultNewArtboardBackgroundColor: DescriptorColor;
5344
- docDefaultNewArtboardBackgroundType: number;
5855
+ docDefaultNewArtboardBackgroundColor?: DescriptorColor;
5856
+ docDefaultNewArtboardBackgroundType?: number;
5345
5857
  }
5346
5858
 
5347
5859
  addHandler(
@@ -5349,17 +5861,15 @@ addHandler(
5349
5861
  target => (target as Psd).artboards !== undefined,
5350
5862
  (reader, target, left) => {
5351
5863
  const desc = readVersionAndDescriptor(reader) as ArtdDescriptor;
5352
- (target as Psd).artboards = {
5353
- count: desc['Cnt '],
5354
- autoExpandOffset: { horizontal: desc.autoExpandOffset.Hrzn, vertical: desc.autoExpandOffset.Vrtc },
5355
- origin: { horizontal: desc.origin.Hrzn, vertical: desc.origin.Vrtc },
5356
- autoExpandEnabled: desc.autoExpandEnabled,
5357
- autoNestEnabled: desc.autoNestEnabled,
5358
- autoPositionEnabled: desc.autoPositionEnabled,
5359
- shrinkwrapOnSaveEnabled: !!desc.shrinkwrapOnSaveEnabled,
5360
- docDefaultNewArtboardBackgroundColor: parseColor(desc.docDefaultNewArtboardBackgroundColor),
5361
- docDefaultNewArtboardBackgroundType: desc.docDefaultNewArtboardBackgroundType,
5362
- };
5864
+ const artboards: Psd['artboards'] = (target as Psd).artboards = { count: desc['Cnt '] };
5865
+ if (desc.autoExpandOffset) artboards.autoExpandOffset = { horizontal: desc.autoExpandOffset.Hrzn, vertical: desc.autoExpandOffset.Vrtc };
5866
+ if (desc.origin) artboards.origin = { horizontal: desc.origin.Hrzn, vertical: desc.origin.Vrtc };
5867
+ if (desc.autoExpandEnabled !== undefined) artboards.autoExpandEnabled = desc.autoExpandEnabled;
5868
+ if (desc.autoNestEnabled !== undefined) artboards.autoNestEnabled = desc.autoNestEnabled;
5869
+ if (desc.autoPositionEnabled !== undefined) artboards.autoPositionEnabled = desc.autoPositionEnabled;
5870
+ artboards.shrinkwrapOnSaveEnabled = !!desc.shrinkwrapOnSaveEnabled;
5871
+ if (desc.docDefaultNewArtboardBackgroundColor) artboards.docDefaultNewArtboardBackgroundColor = parseColor(desc.docDefaultNewArtboardBackgroundColor);
5872
+ if (desc.docDefaultNewArtboardBackgroundType !== undefined) artboards.docDefaultNewArtboardBackgroundType = desc.docDefaultNewArtboardBackgroundType;
5363
5873
 
5364
5874
  skipBytes(reader, left());
5365
5875
  },
@@ -5380,13 +5890,17 @@ addHandler(
5380
5890
  },
5381
5891
  );
5382
5892
 
5893
+ function isGroupLayer(target: LayerAdditionalInfo) {
5894
+ return !!(target as Layer).children;
5895
+ }
5896
+
5383
5897
  export function hasMultiEffects(effects: LayerEffectsInfo) {
5384
5898
  return Object.keys(effects).map(key => (effects as any)[key]).some(v => Array.isArray(v) && v.length > 1);
5385
5899
  }
5386
5900
 
5387
5901
  addHandler(
5388
5902
  'lfx2',
5389
- target => target.effects !== undefined && !hasMultiEffects(target.effects),
5903
+ target => target.effects !== undefined && !isGroupLayer(target) && !hasMultiEffects(target.effects),
5390
5904
  (reader, target, left) => {
5391
5905
  const version = readUint32(reader);
5392
5906
  if (version !== 0) throw new Error(`Invalid lfx2 version`);
@@ -5394,14 +5908,13 @@ addHandler(
5394
5908
  const desc: Lfx2Descriptor & LmfxDescriptor = readVersionAndDescriptor(reader);
5395
5909
  // console.log('READ', require('util').inspect(desc, false, 99, true));
5396
5910
 
5397
- // TODO: don't discard if we got it from lmfx
5398
- // discard if read in 'lrFX' section
5399
- target.effects = parseEffects(desc, !!reader.logMissingFeatures);
5911
+ // discard if read in 'lrFX' section ('lmfx' and 'lfx2' are never stored together)
5912
+ target.effects = parseEffects(desc, reader);
5400
5913
 
5401
5914
  skipBytes(reader, left());
5402
5915
  },
5403
- (writer, target, _, options) => {
5404
- const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true);
5916
+ (writer, target) => {
5917
+ const desc = serializeEffects(target.effects!, writer, true);
5405
5918
  // console.log('WRITE', require('util').inspect(desc, false, 99, true));
5406
5919
 
5407
5920
  writeUint32(writer, 0); // version
@@ -5469,26 +5982,102 @@ addHandler(
5469
5982
  },
5470
5983
  );
5471
5984
 
5472
- interface ExtensionDesc {
5473
- generatorSettings: {
5474
- generator_45_assets: { json: string; };
5475
- layerTime: number;
5476
- };
5985
+ // generator settings of the document (layers store them in 'shmd' section)
5986
+ addHandler(
5987
+ 'extn',
5988
+ target => target.generatorSettings !== undefined && isDocument(target),
5989
+ (reader, target, left) => {
5990
+ const desc = readVersionAndDescriptor(reader) as GeneratorSettingsDescriptor;
5991
+ if (desc.generatorSettings) target.generatorSettings = parseGeneratorSettings(desc.generatorSettings);
5992
+ skipBytes(reader, left());
5993
+ },
5994
+ (writer, target) => {
5995
+ writeVersionAndDescriptor(writer, '', 'null', serializeGeneratorSettings(target.generatorSettings!));
5996
+ },
5997
+ );
5998
+
5999
+ // generator settings stored by older versions of Photoshop (written back in 'shmd' or 'extn' section)
6000
+ addHandler(
6001
+ 'extd',
6002
+ () => false,
6003
+ (reader, target, left) => {
6004
+ const desc = readVersionAndDescriptor(reader) as GeneratorSettingsDescriptor;
6005
+ if (desc.generatorSettings && target.generatorSettings === undefined) {
6006
+ target.generatorSettings = parseGeneratorSettings(desc.generatorSettings);
6007
+ }
6008
+ skipBytes(reader, left());
6009
+ },
6010
+ () => { },
6011
+ );
6012
+
6013
+ interface FrgbDescriptor {
6014
+ framedGroupRect: { _name: ''; _classID: 'classFloatRect'; 'Top ': number; Left: number; Btom: number; Rght: number; };
6015
+ framedGroupType: number;
6016
+ LockPPI?: boolean;
6017
+ PreferredResolution?: number;
6018
+ SavedIteratedName?: string;
5477
6019
  }
5478
6020
 
5479
- // extension settings ?, ignore it
5480
6021
  addHandler(
5481
- 'extn',
5482
- target => (target as any)._extn !== undefined,
5483
- (reader, target) => {
5484
- const desc: ExtensionDesc = readVersionAndDescriptor(reader);
5485
- // console.log(require('util').inspect(desc, false, 99, true));
6022
+ 'frgb', // frame tool group
6023
+ hasKey('framedGroup'),
6024
+ (reader, target, left) => {
6025
+ const desc = readVersionAndDescriptor(reader) as FrgbDescriptor;
6026
+ const rect = desc.framedGroupRect;
6027
+ target.framedGroup = {
6028
+ rect: { top: rect['Top '], left: rect.Left, bottom: rect.Btom, right: rect.Rght },
6029
+ type: desc.framedGroupType,
6030
+ };
6031
+ if (desc.LockPPI !== undefined) target.framedGroup.lockPPI = desc.LockPPI;
6032
+ if (desc.PreferredResolution !== undefined) target.framedGroup.preferredResolution = desc.PreferredResolution;
6033
+ if (desc.SavedIteratedName !== undefined) target.framedGroup.savedIteratedName = desc.SavedIteratedName;
6034
+ skipBytes(reader, left());
6035
+ },
6036
+ (writer, target) => {
6037
+ const group = target.framedGroup!;
6038
+ const rect = group.rect;
6039
+ const desc: FrgbDescriptor = {
6040
+ framedGroupRect: { _name: '', _classID: 'classFloatRect', 'Top ': rect.top, Left: rect.left, Btom: rect.bottom, Rght: rect.right },
6041
+ framedGroupType: group.type,
6042
+ };
6043
+ if (group.lockPPI !== undefined) desc.LockPPI = group.lockPPI;
6044
+ if (group.preferredResolution !== undefined) desc.PreferredResolution = group.preferredResolution;
6045
+ if (group.savedIteratedName !== undefined) desc.SavedIteratedName = group.savedIteratedName;
6046
+ writeVersionAndDescriptor(writer, '', 'framedGroup', desc);
6047
+ },
6048
+ );
5486
6049
 
5487
- if (MOCK_HANDLERS) (target as any)._extn = desc;
6050
+ addHandler(
6051
+ 'anFX',
6052
+ hasKey('animationEffects'),
6053
+ (reader, target, left) => {
6054
+ target.animationEffects = readBytes(reader, left());
5488
6055
  },
5489
6056
  (writer, target) => {
5490
- // TODO: need to add correct types for desc fields (resources/src.psd)
5491
- if (MOCK_HANDLERS) writeVersionAndDescriptor(writer, '', 'null', (target as any)._extn);
6057
+ writeBytes(writer, target.animationEffects);
6058
+ },
6059
+ );
6060
+
6061
+ addHandler(
6062
+ 'PxSD', // pixel source data 2 (video layer frames)
6063
+ hasKey('pixelSourceData'),
6064
+ (reader, target, left) => {
6065
+ target.pixelSourceData = [];
6066
+
6067
+ while (left() >= 8) {
6068
+ const length = readLength64(reader);
6069
+ if (length > left()) throw new Error('Invalid PxSD data length');
6070
+ target.pixelSourceData.push(readBytes(reader, length));
6071
+ }
6072
+
6073
+ skipBytes(reader, left());
6074
+ },
6075
+ (writer, target) => {
6076
+ for (const data of target.pixelSourceData!) {
6077
+ writeUint32(writer, 0);
6078
+ writeUint32(writer, data.byteLength);
6079
+ writeBytes(writer, data);
6080
+ }
5492
6081
  },
5493
6082
  );
5494
6083