ag-psd 32.0.0 → 33.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 (103) 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/932f0770 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  22. package/CHANGELOG.md +78 -0
  23. package/LICENSE +5 -0
  24. package/README.md +3 -6
  25. package/README_PSD.md +242 -81
  26. package/dist/abr.d.ts +11 -4
  27. package/dist/abr.js +357 -230
  28. package/dist/abr.js.map +1 -1
  29. package/dist/additionalInfo.d.ts +54 -2
  30. package/dist/additionalInfo.js +487 -306
  31. package/dist/additionalInfo.js.map +1 -1
  32. package/dist/ase.d.ts +6 -1
  33. package/dist/ase.js +61 -61
  34. package/dist/ase.js.map +1 -1
  35. package/dist/bundle.js +1937 -1115
  36. package/dist/descriptor.d.ts +14 -8
  37. package/dist/descriptor.js +202 -186
  38. package/dist/descriptor.js.map +1 -1
  39. package/dist/helpers.d.ts +1 -1
  40. package/dist/helpers.js +4 -2
  41. package/dist/helpers.js.map +1 -1
  42. package/dist/imageResources.d.ts +15 -3
  43. package/dist/imageResources.js +503 -142
  44. package/dist/imageResources.js.map +1 -1
  45. package/dist/jpeg.js +8 -8
  46. package/dist/jpeg.js.map +1 -1
  47. package/dist/psd.d.ts +148 -17
  48. package/dist/psd.js.map +1 -1
  49. package/dist/psdReader.d.ts +10 -0
  50. package/dist/psdReader.js +278 -166
  51. package/dist/psdReader.js.map +1 -1
  52. package/dist/psdWriter.d.ts +1 -0
  53. package/dist/psdWriter.js +77 -62
  54. package/dist/psdWriter.js.map +1 -1
  55. package/dist/text.js +17 -6
  56. package/dist/text.js.map +1 -1
  57. package/dist/utf8.js +0 -3
  58. package/dist/utf8.js.map +1 -1
  59. package/dist-es/abr.d.ts +11 -4
  60. package/dist-es/abr.js +357 -230
  61. package/dist-es/abr.js.map +1 -1
  62. package/dist-es/additionalInfo.d.ts +54 -2
  63. package/dist-es/additionalInfo.js +487 -306
  64. package/dist-es/additionalInfo.js.map +1 -1
  65. package/dist-es/ase.d.ts +6 -1
  66. package/dist-es/ase.js +61 -61
  67. package/dist-es/ase.js.map +1 -1
  68. package/dist-es/descriptor.d.ts +14 -8
  69. package/dist-es/descriptor.js +202 -186
  70. package/dist-es/descriptor.js.map +1 -1
  71. package/dist-es/helpers.d.ts +1 -1
  72. package/dist-es/helpers.js +4 -2
  73. package/dist-es/helpers.js.map +1 -1
  74. package/dist-es/imageResources.d.ts +15 -3
  75. package/dist-es/imageResources.js +503 -142
  76. package/dist-es/imageResources.js.map +1 -1
  77. package/dist-es/jpeg.js +8 -8
  78. package/dist-es/jpeg.js.map +1 -1
  79. package/dist-es/psd.d.ts +148 -17
  80. package/dist-es/psd.js.map +1 -1
  81. package/dist-es/psdReader.d.ts +10 -0
  82. package/dist-es/psdReader.js +278 -166
  83. package/dist-es/psdReader.js.map +1 -1
  84. package/dist-es/psdWriter.d.ts +1 -0
  85. package/dist-es/psdWriter.js +77 -62
  86. package/dist-es/psdWriter.js.map +1 -1
  87. package/dist-es/text.js +17 -6
  88. package/dist-es/text.js.map +1 -1
  89. package/dist-es/utf8.js +0 -3
  90. package/dist-es/utf8.js.map +1 -1
  91. package/package.json +2 -2
  92. package/src/abr.ts +375 -235
  93. package/src/additionalInfo.ts +663 -420
  94. package/src/ase.ts +66 -61
  95. package/src/descriptor.ts +199 -195
  96. package/src/helpers.ts +4 -1
  97. package/src/imageResources.ts +632 -168
  98. package/src/jpeg.ts +8 -8
  99. package/src/psd.ts +156 -30
  100. package/src/psdReader.ts +273 -172
  101. package/src/psdWriter.ts +95 -58
  102. package/src/text.ts +17 -6
  103. package/src/utf8.ts +0 -4
@@ -1,8 +1,8 @@
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 } 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
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';
8
8
  import { serializeEngineData, parseEngineData } from './engineData';
@@ -103,7 +103,8 @@ addHandler(
103
103
  // require('fs').writeFileSync('after.txt', require('util').inspect(after, false, 99, false), 'utf8');
104
104
 
105
105
  // console.log(require('util').inspect(parseEngineData(text.EngineData), false, 99, true));
106
- target.text = { ...target.text, ...textData };
106
+ // descriptor anti-alias value is more reliable than the one from engine data
107
+ target.text = { ...target.text, ...textData, antiAlias: target.text!.antiAlias ?? textData.antiAlias };
107
108
  // console.log(require('util').inspect(target.text, false, 99, true));
108
109
  }
109
110
 
@@ -228,7 +229,7 @@ function writeBezierKnot(writer: PsdWriter, points: number[], width: number, hei
228
229
 
229
230
  export const booleanOperations: BooleanOperation[] = ['exclude', 'combine', 'subtract', 'intersect'];
230
231
 
231
- export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, width: number, height: number, size: number) {
232
+ export function readVectorMask(reader: PsdReader, vectorMask: VectorPathData, width: number, height: number, size: number) {
232
233
  const end = reader.offset + size;
233
234
  const paths = vectorMask.paths;
234
235
  let path: BezierPath | undefined = undefined;
@@ -283,6 +284,51 @@ export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, w
283
284
  return paths;
284
285
  }
285
286
 
287
+ // componentIndices - write index of path component for each subpath (Photoshop writes them for layer vector masks)
288
+ export function writeVectorMask(writer: PsdWriter, vectorMask: VectorPathData, width: number, height: number, componentIndices = false) {
289
+ // initial entry
290
+ writeUint16(writer, 6);
291
+ writeZeros(writer, 24);
292
+
293
+ const clipboard = vectorMask.clipboard;
294
+ if (clipboard) {
295
+ writeUint16(writer, 7);
296
+ writeFixedPointPath32(writer, clipboard.top);
297
+ writeFixedPointPath32(writer, clipboard.left);
298
+ writeFixedPointPath32(writer, clipboard.bottom);
299
+ writeFixedPointPath32(writer, clipboard.right);
300
+ writeFixedPointPath32(writer, clipboard.resolution);
301
+ writeZeros(writer, 4);
302
+ }
303
+
304
+ writeUint16(writer, 8);
305
+ writeUint16(writer, vectorMask.fillStartsWithAllPixels ? 1 : 0);
306
+ writeZeros(writer, 22);
307
+
308
+ let componentIndex = -1;
309
+
310
+ for (const path of vectorMask.paths) {
311
+ // subpaths without operation are part of the same path component as previous subpath
312
+ if (path.operation || componentIndex < 0) componentIndex++;
313
+
314
+ writeUint16(writer, path.open ? 3 : 0);
315
+ writeUint16(writer, path.knots.length);
316
+ writeUint16(writer, path.operation ? booleanOperations.indexOf(path.operation) : -1); // -1 for undefined
317
+ writeUint16(writer, path.fillRule === 'non-zero' ? 2 : 1);
318
+ writeZeros(writer, 4);
319
+ writeUint32(writer, componentIndices ? componentIndex : 0);
320
+ writeZeros(writer, 10);
321
+
322
+ const linkedKnot = path.open ? 4 : 1;
323
+ const unlinkedKnot = path.open ? 5 : 2;
324
+
325
+ for (const { linked, points } of path.knots) {
326
+ writeUint16(writer, linked ? linkedKnot : unlinkedKnot);
327
+ writeBezierKnot(writer, points, width, height);
328
+ }
329
+ }
330
+ }
331
+
286
332
  addHandler(
287
333
  'vmsk',
288
334
  hasKey('vectorMask'),
@@ -312,45 +358,11 @@ addHandler(
312
358
 
313
359
  writeUint32(writer, 3); // version
314
360
  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
- }
361
+ writeVectorMask(writer, vectorMask, width, height, true);
350
362
  },
351
363
  );
352
364
 
353
- // TODO: need to write vmsk if has outline ?
365
+ // written instead of 'vmsk' for layers with vector stroke ('vscg' section)
354
366
  addHandlerAlias('vsms', 'vmsk');
355
367
  // addHandlerAlias('vmsk', 'vsms');
356
368
 
@@ -365,7 +377,7 @@ addHandler(
365
377
  },
366
378
  );
367
379
 
368
- interface VogkDescriptor {
380
+ export interface KeyDescriptorListDescriptor {
369
381
  keyDescriptorList: {
370
382
  keyShapeInvalidated?: boolean;
371
383
  keyOriginType?: number;
@@ -395,121 +407,128 @@ interface VogkDescriptor {
395
407
  }[];
396
408
  }
397
409
 
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: [] };
410
+ export function parseKeyDescriptorList(desc: KeyDescriptorListDescriptor) {
411
+ const keyDescriptorList: KeyDescriptorItem[] = [];
412
+
413
+ for (const i of desc.keyDescriptorList) {
414
+ const item: KeyDescriptorItem = {};
415
+
416
+ if (i.keyShapeInvalidated != null) item.keyShapeInvalidated = i.keyShapeInvalidated;
417
+ if (i.keyOriginType != null) item.keyOriginType = i.keyOriginType;
418
+ if (i.keyOriginResolution != null) item.keyOriginResolution = i.keyOriginResolution;
419
+ if (i.keyOriginShapeBBox) {
420
+ item.keyOriginShapeBoundingBox = {
421
+ top: parseUnitsOrNumber(i.keyOriginShapeBBox['Top ']),
422
+ left: parseUnitsOrNumber(i.keyOriginShapeBBox.Left),
423
+ bottom: parseUnitsOrNumber(i.keyOriginShapeBBox.Btom),
424
+ right: parseUnitsOrNumber(i.keyOriginShapeBBox.Rght),
425
+ };
426
+ }
427
+ const rectRadii = i.keyOriginRRectRadii;
428
+ if (rectRadii) {
429
+ item.keyOriginRRectRadii = {
430
+ topRight: parseUnits(rectRadii.topRight),
431
+ topLeft: parseUnits(rectRadii.topLeft),
432
+ bottomLeft: parseUnits(rectRadii.bottomLeft),
433
+ bottomRight: parseUnits(rectRadii.bottomRight),
434
+ };
435
+ }
436
+ const corners = i.keyOriginBoxCorners;
437
+ if (corners) {
438
+ item.keyOriginBoxCorners = [
439
+ { x: corners.rectangleCornerA.Hrzn, y: corners.rectangleCornerA.Vrtc },
440
+ { x: corners.rectangleCornerB.Hrzn, y: corners.rectangleCornerB.Vrtc },
441
+ { x: corners.rectangleCornerC.Hrzn, y: corners.rectangleCornerC.Vrtc },
442
+ { x: corners.rectangleCornerD.Hrzn, y: corners.rectangleCornerD.Vrtc },
443
+ ];
444
+ }
445
+ const trnf = i.Trnf;
446
+ if (trnf) {
447
+ item.transform = [trnf.xx, trnf.xy, trnf.yx, trnf.yy, trnf.tx, trnf.ty];
448
+ }
407
449
 
408
- for (const i of desc.keyDescriptorList) {
409
- const item: KeyDescriptorItem = {};
450
+ keyDescriptorList.push(item);
451
+ }
410
452
 
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
- }
453
+ return keyDescriptorList;
454
+ }
444
455
 
445
- target.vectorOrigination.keyDescriptorList.push(item);
446
- }
456
+ export function serializeKeyDescriptorList(keyDescriptorList: KeyDescriptorItem[]) {
457
+ const desc: KeyDescriptorListDescriptor = { keyDescriptorList: [] };
447
458
 
448
- skipBytes(reader, left());
449
- },
450
- (writer, target) => {
451
- target;
452
- const orig = target.vectorOrigination!;
453
- const desc: VogkDescriptor = { keyDescriptorList: [] };
459
+ for (let i = 0; i < keyDescriptorList.length; i++) {
460
+ const item = keyDescriptorList[i];
454
461
 
455
- for (let i = 0; i < orig.keyDescriptorList.length; i++) {
456
- const item = orig.keyDescriptorList[i];
462
+ desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
457
463
 
458
- desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
464
+ const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1];
459
465
 
460
- const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1];
466
+ if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType;
467
+ if (item.keyOriginResolution != null) out.keyOriginResolution = item.keyOriginResolution;
461
468
 
462
- if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType;
463
- if (item.keyOriginResolution != null) out.keyOriginResolution = item.keyOriginResolution;
469
+ const radii = item.keyOriginRRectRadii;
470
+ if (radii) {
471
+ out.keyOriginRRectRadii = {
472
+ unitValueQuadVersion: 1,
473
+ topRight: unitsValue(radii.topRight, 'topRight'),
474
+ topLeft: unitsValue(radii.topLeft, 'topLeft'),
475
+ bottomLeft: unitsValue(radii.bottomLeft, 'bottomLeft'),
476
+ bottomRight: unitsValue(radii.bottomRight, 'bottomRight'),
477
+ };
478
+ }
464
479
 
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
- }
480
+ const box = item.keyOriginShapeBoundingBox;
481
+ if (box) {
482
+ out.keyOriginShapeBBox = {
483
+ unitValueQuadVersion: 1,
484
+ 'Top ': unitsValue(box.top, 'top'),
485
+ Left: unitsValue(box.left, 'left'),
486
+ Btom: unitsValue(box.bottom, 'bottom'),
487
+ Rght: unitsValue(box.right, 'right'),
488
+ };
489
+ }
475
490
 
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
- }
491
+ const corners = item.keyOriginBoxCorners;
492
+ if (corners && corners.length === 4) {
493
+ out.keyOriginBoxCorners = {
494
+ rectangleCornerA: { Hrzn: corners[0].x, Vrtc: corners[0].y },
495
+ rectangleCornerB: { Hrzn: corners[1].x, Vrtc: corners[1].y },
496
+ rectangleCornerC: { Hrzn: corners[2].x, Vrtc: corners[2].y },
497
+ rectangleCornerD: { Hrzn: corners[3].x, Vrtc: corners[3].y },
498
+ };
499
+ }
486
500
 
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
- }
501
+ const transform = item.transform;
502
+ if (transform && transform.length === 6) {
503
+ out.Trnf = {
504
+ xx: transform[0],
505
+ xy: transform[1],
506
+ yx: transform[2],
507
+ yy: transform[3],
508
+ tx: transform[4],
509
+ ty: transform[5],
510
+ };
511
+ }
496
512
 
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
- }
513
+ if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated;
514
+ out.keyOriginIndex = i;
515
+ }
508
516
 
509
- if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated;
510
- out.keyOriginIndex = i;
511
- }
517
+ return desc;
518
+ }
512
519
 
520
+ addHandler(
521
+ 'vogk',
522
+ hasKey('vectorOrigination'),
523
+ (reader, target, left) => {
524
+ if (readInt32(reader) !== 1) throw new Error(`Invalid vogk version`);
525
+ const desc = readVersionAndDescriptor(reader) as KeyDescriptorListDescriptor;
526
+ // console.log(require('util').inspect(desc, false, 99, true));
527
+ target.vectorOrigination = { keyDescriptorList: parseKeyDescriptorList(desc) };
528
+ skipBytes(reader, left());
529
+ },
530
+ (writer, target) => {
531
+ const desc = serializeKeyDescriptorList(target.vectorOrigination!.keyDescriptorList);
513
532
  writeInt32(writer, 1); // version
514
533
  writeVersionAndDescriptor(writer, '', 'null', desc);
515
534
  }
@@ -517,7 +536,7 @@ addHandler(
517
536
 
518
537
  addHandler(
519
538
  'lmfx',
520
- target => target.effects !== undefined && hasMultiEffects(target.effects),
539
+ target => target.effects !== undefined && !isGroupLayer(target) && hasMultiEffects(target.effects),
521
540
  (reader, target, left) => {
522
541
  const version = readUint32(reader);
523
542
  if (version !== 0) throw new Error('Invalid lmfx version');
@@ -621,7 +640,12 @@ addHandler(
621
640
 
622
641
  if (target.id !== undefined) {
623
642
  id = target.id;
624
- while (options.layerIds.has(id)) id += 100; // make sure we don't have duplicate layer ids
643
+
644
+ if (options.layerIds.has(id)) {
645
+ // make sure we don't have duplicate layer ids (and don't take id explicitly set on another layer)
646
+ const explicitIds = getExplicitLayerIds(psd, options);
647
+ while (options.layerIds.has(id) || explicitIds.has(id)) id += 100;
648
+ }
625
649
  } else {
626
650
  // generate id for layers that don't have it, if it's referenced by other sections
627
651
  const explicitIds = getExplicitLayerIds(psd, options);
@@ -733,15 +757,17 @@ addHandler(
733
757
  };
734
758
 
735
759
  if (flags & 0x08) target.protected.artboards = true;
760
+ if (flags & 0x80000000) target.protected.all = true;
736
761
  },
737
762
  (writer, target) => {
738
763
  const flags =
739
764
  (target.protected!.transparency ? 0x01 : 0) |
740
765
  (target.protected!.composite ? 0x02 : 0) |
741
766
  (target.protected!.position ? 0x04 : 0) |
742
- (target.protected!.artboards ? 0x08 : 0);
767
+ (target.protected!.artboards ? 0x08 : 0) |
768
+ (target.protected!.all ? 0x80000000 : 0);
743
769
 
744
- writeUint32(writer, flags);
770
+ writeUint32(writer, flags >>> 0);
745
771
  },
746
772
  );
747
773
 
@@ -764,6 +790,62 @@ interface CustomDescriptor {
764
790
  layerTime?: number;
765
791
  }
766
792
 
793
+ interface GeneratorSettingsDescriptor {
794
+ generatorSettings?: {
795
+ _name: '';
796
+ _classID: string; // 'null' or 'generatorSettings'
797
+ exportAs?: {
798
+ _name: '';
799
+ _classID: 'null';
800
+ exportOption?: string; // JSON
801
+ scaleSettings?: string; // JSON
802
+ };
803
+ generator_45_assets?: { _name: ''; _classID: string; json: string; };
804
+ layerTime?: number;
805
+ crema?: { _name: ''; _classID: string; json: string; };
806
+ };
807
+ }
808
+
809
+ function parseGeneratorSettings(settings: NonNullable<GeneratorSettingsDescriptor['generatorSettings']>) {
810
+ const result: NonNullable<LayerAdditionalInfo['generatorSettings']> = {};
811
+
812
+ if (settings.exportAs) {
813
+ result.exportAs = {};
814
+ if (settings.exportAs.exportOption !== undefined) result.exportAs.exportOption = settings.exportAs.exportOption;
815
+ if (settings.exportAs.scaleSettings !== undefined) result.exportAs.scaleSettings = settings.exportAs.scaleSettings;
816
+ }
817
+
818
+ if (settings.generator_45_assets) result.assets = settings.generator_45_assets.json;
819
+ if (settings.layerTime !== undefined) result.layerTime = settings.layerTime;
820
+ if (settings.crema) result.crema = settings.crema.json;
821
+
822
+ return result;
823
+ }
824
+
825
+ function serializeGeneratorSettings(generatorSettings: NonNullable<LayerAdditionalInfo['generatorSettings']>): GeneratorSettingsDescriptor {
826
+ const settings: GeneratorSettingsDescriptor['generatorSettings'] = {
827
+ _name: '',
828
+ _classID: generatorSettings.assets !== undefined ? 'generatorSettings' : 'null',
829
+ };
830
+
831
+ if (generatorSettings.exportAs) {
832
+ settings.exportAs = { _name: '', _classID: 'null' };
833
+ if (generatorSettings.exportAs.exportOption !== undefined) settings.exportAs.exportOption = generatorSettings.exportAs.exportOption;
834
+ if (generatorSettings.exportAs.scaleSettings !== undefined) settings.exportAs.scaleSettings = generatorSettings.exportAs.scaleSettings;
835
+ }
836
+
837
+ if (generatorSettings.assets !== undefined) settings.generator_45_assets = { _name: '', _classID: 'generatorSettings', json: generatorSettings.assets };
838
+ if (generatorSettings.layerTime !== undefined) settings.layerTime = generatorSettings.layerTime;
839
+ if (generatorSettings.crema !== undefined) settings.crema = { _name: '', _classID: 'null', json: generatorSettings.crema };
840
+
841
+ return { generatorSettings: settings };
842
+ }
843
+
844
+ // document (Psd object) stores generator settings in 'extn' section instead of 'shmd' section
845
+ function isDocument(target: LayerAdditionalInfo) {
846
+ return (target as Psd).width !== undefined;
847
+ }
848
+
767
849
  interface CmlsDescriptor {
768
850
  origFXRefPoint?: { Hrzn: number; Vrtc: number; };
769
851
  LyrI: number;
@@ -777,112 +859,113 @@ interface CmlsDescriptor {
777
859
 
778
860
  addHandler(
779
861
  'shmd', // Metadata setting
780
- target => target.timestamp !== undefined || target.animationFrames !== undefined || target.animationFrameFlags !== undefined || target.timeline !== undefined || target.comps !== undefined,
862
+ target => target.timestamp !== undefined || target.animationFrames !== undefined || target.animationFrameFlags !== undefined || target.timeline !== undefined || target.comps !== undefined ||
863
+ (target.generatorSettings !== undefined && !isDocument(target)),
781
864
  (reader, target, left) => {
782
865
  const count = readUint32(reader);
783
866
 
784
867
  for (let i = 0; i < count; i++) {
785
- checkSignature(reader, '8BIM');
868
+ const signature = readSignature(reader);
869
+ if (signature !== '8BIM' && signature !== '8ELE') throw new Error(`Invalid shmd signature: '${signature}'`); // 8ELE in Photoshop Elements
786
870
  const key = readSignature(reader);
787
871
  readUint8(reader); // copy
788
872
  skipBytes(reader, 3);
789
873
 
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
- };
874
+ const end = reader.offset + 4 + reader.view.getUint32(reader.offset);
875
+
876
+ try {
877
+ readSection(reader, 1, left => {
878
+ if (key === 'cust') {
879
+ const desc = readVersionAndDescriptor(reader) as CustomDescriptor;
880
+ // console.log('cust', target.name, require('util').inspect(desc, false, 99, true));
881
+ if (desc.layerTime !== undefined) target.timestamp = desc.layerTime;
882
+ } else if (key === 'mlst') {
883
+ const desc = readVersionAndDescriptor(reader) as FrameListDescriptor;
884
+ // console.log('mlst', target.name, require('util').inspect(desc, false, 99, true));
885
+
886
+ target.animationFrames = [];
887
+
888
+ for (let i = 0; i < desc.LaSt.length; i++) {
889
+ const f = desc.LaSt[i];
890
+ const frame: AnimationFrame = { frames: f.FrLs };
891
+ if (f.enab !== undefined) frame.enable = f.enab;
892
+ if (f.Ofst) frame.offset = horzVrtcToXY(f.Ofst);
893
+ if (f.FXRf) frame.referencePoint = horzVrtcToXY(f.FXRf);
894
+ if (f.Lefx) frame.effects = parseEffects(f.Lefx, !!reader.logMissingFeatures);
895
+ if (f.blendOptions && f.blendOptions.Opct) frame.opacity = parsePercent(f.blendOptions.Opct);
896
+ target.animationFrames.push(frame);
897
+ }
898
+ } else if (key === 'mdyn') {
899
+ // frame flags
900
+ readUint16(reader); // unknown
901
+ const propagate = readUint8(reader);
902
+ const flags = readUint8(reader);
903
+
904
+ target.animationFrameFlags = {
905
+ propagateFrameOne: !propagate,
906
+ unifyLayerPosition: (flags & 1) !== 0,
907
+ unifyLayerStyle: (flags & 2) !== 0,
908
+ unifyLayerVisibility: (flags & 4) !== 0,
909
+ };
910
+ } else if (key === 'tmln') {
911
+ const desc = readVersionAndDescriptor(reader) as TimelineDescriptor;
912
+ const timeScope = desc.timeScope;
913
+ // console.log('tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
914
+
915
+ const timeline: Timeline = {
916
+ start: frac(timeScope.Strt),
917
+ duration: frac(timeScope.duration),
918
+ inTime: frac(timeScope.inTime),
919
+ outTime: frac(timeScope.outTime),
920
+ autoScope: desc.autoScope,
921
+ audioLevel: desc.audioLevel,
922
+ };
836
923
 
837
- if (desc.trackList) {
838
- timeline.tracks = parseTrackList(desc.trackList, !!reader.logMissingFeatures);
839
- }
924
+ if (desc.trackList) {
925
+ timeline.tracks = parseTrackList(desc.trackList, !!reader.logMissingFeatures);
926
+ }
840
927
 
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));
928
+ target.timeline = timeline;
929
+ // console.log('tmln:result', target.name, target.id, require('util').inspect(timeline, false, 99, true));
930
+ } else if (key === 'cmls') {
931
+ const desc = readVersionAndDescriptor(reader) as CmlsDescriptor;
932
+ // console.log('cmls', require('util').inspect(desc, false, 99, true));
846
933
 
847
- target.comps = {
848
- settings: [],
849
- };
934
+ target.comps = {
935
+ settings: [],
936
+ };
850
937
 
851
- if (desc.origFXRefPoint) target.comps.originalEffectsReferencePoint = { x: desc.origFXRefPoint.Hrzn, y: desc.origFXRefPoint.Vrtc };
938
+ if (desc.origFXRefPoint) target.comps.originalEffectsReferencePoint = { x: desc.origFXRefPoint.Hrzn, y: desc.origFXRefPoint.Vrtc };
852
939
 
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
- };
940
+ for (const item of desc.layerSettings) {
941
+ target.comps.settings.push({ compList: item.compList });
942
+ const t = target.comps.settings[target.comps.settings.length - 1];
943
+ if ('enab' in item) t.enabled = item.enab;
944
+ if (item.Ofst) t.offset = { x: item.Ofst.Hrzn, y: item.Ofst.Vrtc };
945
+ if (item.FXRefPoint) t.effectsReferencePoint = { x: item.FXRefPoint.Hrzn, y: item.FXRefPoint.Vrtc };
946
+ }
947
+ } else if (key === 'extn') {
948
+ const desc = readVersionAndDescriptor(reader) as GeneratorSettingsDescriptor;
949
+ if (desc.generatorSettings) target.generatorSettings = parseGeneratorSettings(desc.generatorSettings);
950
+ } else {
951
+ reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key);
868
952
  }
869
953
 
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
- });
954
+ skipBytes(reader, left());
955
+ });
956
+ } catch (e) {
957
+ // failure to read one item shouldn't drop all other items
958
+ if (reader.throwForMissingFeatures) throw e;
959
+ reader.logMissingFeatures && reader.log(`Failed to read "shmd" section key ${key}: ${(e as Error).message}`);
960
+ reader.offset = end;
961
+ }
880
962
  }
881
963
 
882
964
  skipBytes(reader, left());
883
965
  },
884
966
  (writer, target, _, options) => {
885
967
  const { animationFrames, animationFrameFlags, timestamp, timeline, comps } = target;
968
+ const generatorSettings = isDocument(target) ? undefined : target.generatorSettings;
886
969
 
887
970
  let count = 0;
888
971
  if (animationFrames) count++;
@@ -890,6 +973,7 @@ addHandler(
890
973
  if (timeline) count++;
891
974
  if (timestamp !== undefined) count++;
892
975
  if (comps) count++;
976
+ if (generatorSettings) count++;
893
977
  writeUint32(writer, count);
894
978
 
895
979
  if (animationFrames) {
@@ -910,8 +994,8 @@ addHandler(
910
994
  frame.FrLs = f.frames;
911
995
  if (f.offset) frame.Ofst = xyToHorzVrtc(f.offset);
912
996
  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) };
997
+ if (f.effects) frame.Lefx = serializeEffects(f.effects, false, hasMultiEffects(f.effects));
998
+ if (f.opacity !== undefined) frame.blendOptions = { Opct: unitsPercentF(f.opacity) };
915
999
  desc.LaSt.push(frame);
916
1000
  }
917
1001
 
@@ -1005,6 +1089,16 @@ addHandler(
1005
1089
  writeVersionAndDescriptor(writer, '', 'null', desc);
1006
1090
  }, true);
1007
1091
  }
1092
+
1093
+ if (generatorSettings) {
1094
+ writeSignature(writer, '8BIM');
1095
+ writeSignature(writer, 'extn');
1096
+ writeUint8(writer, 0); // copy (always false)
1097
+ writeZeros(writer, 3);
1098
+ writeSection(writer, 2, () => {
1099
+ writeVersionAndDescriptor(writer, '', 'null', serializeGeneratorSettings(generatorSettings));
1100
+ }, true);
1101
+ }
1008
1102
  },
1009
1103
  );
1010
1104
 
@@ -1096,7 +1190,7 @@ addHandler(
1096
1190
  lineAlignment: strokeStyleLineAlignment.decode(desc.strokeStyleLineAlignment),
1097
1191
  scaleLock: desc.strokeStyleScaleLock,
1098
1192
  strokeAdjust: desc.strokeStyleStrokeAdjust,
1099
- lineDashSet: desc.strokeStyleLineDashSet.map(parseUnits),
1193
+ lineDashSet: (desc.strokeStyleLineDashSet || []).map(parseUnits),
1100
1194
  blendMode: BlnM.decode(desc.strokeStyleBlendMode),
1101
1195
  opacity: parsePercent(desc.strokeStyleOpacity),
1102
1196
  content: parseVectorContent(desc.strokeStyleContent),
@@ -1135,8 +1229,8 @@ interface ArtbDescriptor {
1135
1229
  artboardRect: { 'Top ': number; Left: number; Btom: number; Rght: number; };
1136
1230
  guideIndeces: any[];
1137
1231
  artboardPresetName: string;
1138
- 'Clr ': DescriptorColor;
1139
- artboardBackgroundType: number;
1232
+ 'Clr '?: DescriptorColor;
1233
+ artboardBackgroundType?: number;
1140
1234
  }
1141
1235
 
1142
1236
  addHandler(
@@ -1149,10 +1243,11 @@ addHandler(
1149
1243
  rect: { top: rect['Top '], left: rect.Left, bottom: rect.Btom, right: rect.Rght },
1150
1244
  guideIndices: desc.guideIndeces,
1151
1245
  presetName: desc.artboardPresetName,
1152
- color: parseColor(desc['Clr ']),
1153
- backgroundType: desc.artboardBackgroundType,
1154
1246
  };
1155
1247
 
1248
+ if (desc['Clr ']) target.artboard.color = parseColor(desc['Clr ']);
1249
+ if (desc.artboardBackgroundType !== undefined) target.artboard.backgroundType = desc.artboardBackgroundType;
1250
+
1156
1251
  skipBytes(reader, left());
1157
1252
  },
1158
1253
  (writer, target) => {
@@ -1299,13 +1394,13 @@ function encodeWarp(warp: Warp): WarpDescriptor {
1299
1394
 
1300
1395
  addHandler(
1301
1396
  'PlLd',
1302
- hasKey('placedLayer'),
1397
+ target => !!target.placedLayer && !target.placedLayer.external, // linked smart objects are stored in SoLE
1303
1398
  (reader, target, left) => {
1304
1399
  if (readSignature(reader) !== 'plcL') throw new Error(`Invalid PlLd signature`);
1305
1400
  if (readInt32(reader) !== 3) throw new Error(`Invalid PlLd version`);
1306
1401
  const id = readPascalString(reader, 1);
1307
1402
  const pageNumber = readInt32(reader);
1308
- const totalPages = readInt32(reader); // TODO: check how this works ?
1403
+ const totalPages = readInt32(reader); // for multi-page documents (PDF)
1309
1404
  readInt32(reader); // anitAliasPolicy 16
1310
1405
  const placedLayerType = readInt32(reader); // 0 = unknown, 1 = vector, 2 = raster, 3 = image stack
1311
1406
  if (!placedLayerTypes[placedLayerType]) throw new Error('Invalid PlLd type');
@@ -1857,7 +1952,7 @@ type SoLdDescriptorFilterItem = {
1857
1952
  'Fl ': string;
1858
1953
  };
1859
1954
  } | {
1860
- filterID: 991 | 943; // TODO: why 2 different IDs? do we need to handle them separately?
1955
+ filterID: 991 | 943; // 943 in older Photoshop versions (2021), 991 in newer ones, settings are the same
1861
1956
  Fltr: {
1862
1957
  _name: 'Rigid Transform';
1863
1958
  _classID: 'rigidTransform';
@@ -2036,7 +2131,7 @@ type SoLdDescriptorFilterItem = {
2036
2131
  Fltr: {
2037
2132
  _name: 'Curves';
2038
2133
  _classID: 'Crvs';
2039
- presetKind: string; // 'presetKindType.presetKindCustom';
2134
+ presetKind?: string; // 'presetKindType.presetKindCustom';
2040
2135
  Adjs?: {
2041
2136
  _name: '';
2042
2137
  _classID: 'CrvA';
@@ -2045,6 +2140,14 @@ type SoLdDescriptorFilterItem = {
2045
2140
  Mpng?: number[];
2046
2141
  }[];
2047
2142
  };
2143
+ } | {
2144
+ filterID: 1282829427;
2145
+ Fltr: {
2146
+ _name: 'Levels';
2147
+ _classID: 'Lvls';
2148
+ presetKind?: string; // 'presetKindType.presetKindCustom';
2149
+ Adjs?: FilterLevelsAdjustment[];
2150
+ };
2048
2151
  } | {
2049
2152
  filterID: 1231976050;
2050
2153
  } | {
@@ -2066,6 +2169,18 @@ interface FilterCurvesCurvePoint {
2066
2169
  Cnty?: boolean;
2067
2170
  }
2068
2171
 
2172
+ interface FilterLevelsAdjustment {
2173
+ _name: '';
2174
+ _classID: 'LvlA';
2175
+ Chnl: string[]; // 'Chnl.Cmps' | 'Chnl.Rd ' | 'Chnl.Grn ' | 'Chnl.Bl '
2176
+ Inpt?: number[]; // shadow, highlight
2177
+ Otpt?: number[]; // shadow, highlight
2178
+ 'Gmm '?: number;
2179
+ }
2180
+
2181
+ const levelsFilterKeys = ['_name', '_classID', 'presetKind', 'Adjs'];
2182
+ const levelsFilterAdjustmentKeys = ['_name', '_classID', 'Chnl', 'Inpt', 'Otpt', 'Gmm '];
2183
+
2069
2184
  interface SoLdDescriptorFilter {
2070
2185
  _name: '',
2071
2186
  _classID: 'filterFXStyle',
@@ -2129,7 +2244,7 @@ function pointToHrznVrtc(point: { x: UnitsValue; y: UnitsValue; }): HrznVrtcDesc
2129
2244
  };
2130
2245
  }
2131
2246
 
2132
- function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): Filter | undefined {
2247
+ function parseFilterFXItem(f: SoLdDescriptorFilterItem, reader: PsdReader): Filter | undefined {
2133
2248
  const base: Omit<Filter, 'type' | 'filter'> = {
2134
2249
  name: f['Nm '],
2135
2250
  opacity: parsePercent(f.blendOptions.Opct),
@@ -2209,7 +2324,7 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2209
2324
  undefinedAreas: UndA.decode(f.Fltr.UndA),
2210
2325
  displacementFile: {
2211
2326
  signature: f.Fltr.DspF.sig,
2212
- path: f.Fltr.DspF.path, // TODO: this is decoded incorrectly ???
2327
+ path: f.Fltr.DspF.path,
2213
2328
  },
2214
2329
  },
2215
2330
  };
@@ -2544,8 +2659,7 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2544
2659
  ],
2545
2660
  puppetShapeList: (f.Fltr.puppetShapeList || []).map(p => ({
2546
2661
  rigidType: p.rigidType,
2547
- // TODO: VrsM
2548
- // TODO: VrsN
2662
+ // VrsM / VrsN - version (always 1.0)
2549
2663
  originalVertexArray: uint8ToPoints(p.originalVertexArray),
2550
2664
  deformedVertexArray: uint8ToPoints(p.deformedVertexArray),
2551
2665
  indexArray: Array.from(uint8ToUint32(p.indexArray)),
@@ -2664,7 +2778,8 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2664
2778
  ...base,
2665
2779
  type: 'curves',
2666
2780
  filter: {
2667
- presetKind: presetKindType.decode(f.Fltr.presetKind),
2781
+ // Photoshop doesn't write preset kind for filters with explicit values
2782
+ ...(f.Fltr.presetKind !== undefined ? { presetKind: presetKindType.decode(f.Fltr.presetKind) } : {}),
2668
2783
  ...(f.Fltr.Adjs ? {
2669
2784
  adjustments: f.Fltr.Adjs.map(a => {
2670
2785
  const channels = a.Chnl.map(Chnl.decode);
@@ -2687,6 +2802,40 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2687
2802
  },
2688
2803
  };
2689
2804
  };
2805
+ case 'Lvls': {
2806
+ const adjustments = f.Fltr.Adjs;
2807
+ const isKnown = (obj: object, keys: string[]) => Object.keys(obj).every(k => keys.indexOf(k) !== -1);
2808
+
2809
+ // skip filter with settings we don't know about
2810
+ if (!isKnown(f.Fltr, levelsFilterKeys) || (adjustments && !adjustments.every(a => isKnown(a, levelsFilterAdjustmentKeys)))) {
2811
+ if (reader.throwForMissingFeatures) throw new Error('Unknown levels filter settings');
2812
+ reader.logMissingFeatures && reader.log(`Skipped levels smart filter with unknown settings`);
2813
+ return undefined;
2814
+ }
2815
+
2816
+ return {
2817
+ ...base,
2818
+ type: 'levels',
2819
+ filter: {
2820
+ ...(f.Fltr.presetKind !== undefined ? { presetKind: presetKindType.decode(f.Fltr.presetKind) } : {}),
2821
+ ...(adjustments ? {
2822
+ adjustments: adjustments.map(a => {
2823
+ const result: LevelsFilterAdjustment = { channels: a.Chnl.map(Chnl.decode) };
2824
+ if (a.Inpt) {
2825
+ result.shadowInput = a.Inpt[0];
2826
+ result.highlightInput = a.Inpt[1];
2827
+ }
2828
+ if (a.Otpt) {
2829
+ result.shadowOutput = a.Otpt[0];
2830
+ result.highlightOutput = a.Otpt[1];
2831
+ }
2832
+ if (a['Gmm '] !== undefined) result.midtoneInput = a['Gmm '];
2833
+ return result;
2834
+ }),
2835
+ } : {}),
2836
+ },
2837
+ };
2838
+ };
2690
2839
  case 'BrgC': {
2691
2840
  return {
2692
2841
  ...base,
@@ -2699,10 +2848,11 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2699
2848
  };
2700
2849
  };
2701
2850
  default:
2702
- if (options.throwForMissingFeatures) {
2851
+ if (reader.throwForMissingFeatures) {
2703
2852
  // console.log('FILTER', require('util').inspect(f, false, 99, true));
2704
2853
  throw new Error(`Unknown filter classId: ${(f as any).Fltr._classID}`);
2705
2854
  }
2855
+ reader.logMissingFeatures && reader.log(`Unhandled smart filter: ${(f as any).Fltr._classID} (${f['Nm ']})`);
2706
2856
  return undefined;
2707
2857
  }
2708
2858
  } else {
@@ -2721,22 +2871,24 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2721
2871
  case 1314149187: return { ...base, type: 'ntsc colors' };
2722
2872
  case 1231976050: return { ...base, type: 'invert' }
2723
2873
  default:
2724
- if (options.throwForMissingFeatures) {
2874
+ if (reader.throwForMissingFeatures) {
2725
2875
  // console.log('FILTER', require('util').inspect(f, false, 99, true));
2726
2876
  throw new Error(`Unknown filterID: ${(f as any).filterID}`);
2727
2877
  }
2878
+ reader.logMissingFeatures && reader.log(`Unhandled smart filter: ${(f as any).filterID} (${f['Nm ']})`);
2879
+ return undefined;
2728
2880
  }
2729
2881
  }
2730
2882
  }
2731
2883
 
2732
- function parseFilterFX(desc: SoLdDescriptorFilter, options: ReadOptions): PlacedLayerFilter {
2884
+ function parseFilterFX(desc: SoLdDescriptorFilter, reader: PsdReader): PlacedLayerFilter {
2733
2885
  return {
2734
2886
  enabled: desc.enab,
2735
2887
  validAtPosition: desc.validAtPosition,
2736
2888
  maskEnabled: desc.filterMaskEnable,
2737
2889
  maskLinked: desc.filterMaskLinked,
2738
2890
  maskExtendWithWhite: desc.filterMaskExtendWithWhite,
2739
- list: desc.filterFXList.map(x => parseFilterFXItem(x, options)).filter((x): x is Filter => !!x),
2891
+ list: desc.filterFXList.map(x => parseFilterFXItem(x, reader)).filter((x): x is Filter => !!x),
2740
2892
  };
2741
2893
  }
2742
2894
 
@@ -3275,14 +3427,14 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3275
3427
  Fltr: {
3276
3428
  _name: 'Rigid Transform',
3277
3429
  _classID: 'rigidTransform',
3278
- 'null': ['Ordn.Trgt'], // TODO: ???
3430
+ 'null': ['Ordn.Trgt'], // target layer reference
3279
3431
  rigidType: f.filter.rigidType,
3280
3432
  puppetShapeList: (f.filter.puppetShapeList || []).map(p => ({
3281
3433
  _name: '',
3282
3434
  _classID: 'puppetShape',
3283
3435
  rigidType: p.rigidType,
3284
- VrsM: 1, // TODO: ???
3285
- VrsN: 0, // TODO: ???
3436
+ VrsM: 1, // version major
3437
+ VrsN: 0, // version minor
3286
3438
  originalVertexArray: toUint8(new Float32Array(pointsToArray(p.originalVertexArray))),
3287
3439
  deformedVertexArray: toUint8(new Float32Array(pointsToArray(p.deformedVertexArray))),
3288
3440
  indexArray: toUint8(new Uint32Array(p.indexArray)),
@@ -3397,7 +3549,7 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3397
3549
  Fltr: {
3398
3550
  _name: 'Curves',
3399
3551
  _classID: 'Crvs',
3400
- presetKind: presetKindType.encode(f.filter.presetKind),
3552
+ ...(f.filter.presetKind !== undefined ? { presetKind: presetKindType.encode(f.filter.presetKind) } : {}),
3401
3553
  ...(f.filter.adjustments ? {
3402
3554
  Adjs: f.filter.adjustments.map(a => 'curve' in a ? {
3403
3555
  _name: '',
@@ -3420,6 +3572,28 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3420
3572
  },
3421
3573
  filterID: 1131574899,
3422
3574
  };
3575
+ case 'levels': return {
3576
+ ...base,
3577
+ Fltr: {
3578
+ _name: 'Levels',
3579
+ _classID: 'Lvls',
3580
+ ...(f.filter.presetKind !== undefined ? { presetKind: presetKindType.encode(f.filter.presetKind) } : {}),
3581
+ ...(f.filter.adjustments ? {
3582
+ Adjs: f.filter.adjustments.map(a => {
3583
+ const adjustment: FilterLevelsAdjustment = { _name: '', _classID: 'LvlA', Chnl: a.channels.map(Chnl.encode) };
3584
+ if (a.shadowInput !== undefined || a.highlightInput !== undefined) {
3585
+ adjustment.Inpt = [Math.round(a.shadowInput ?? 0), Math.round(a.highlightInput ?? 255)];
3586
+ }
3587
+ if (a.shadowOutput !== undefined || a.highlightOutput !== undefined) {
3588
+ adjustment.Otpt = [Math.round(a.shadowOutput ?? 0), Math.round(a.highlightOutput ?? 255)];
3589
+ }
3590
+ if (a.midtoneInput !== undefined) adjustment['Gmm '] = a.midtoneInput;
3591
+ return adjustment;
3592
+ }),
3593
+ } : {}),
3594
+ },
3595
+ filterID: 1282829427,
3596
+ };
3423
3597
  case 'brightness/contrast': return {
3424
3598
  ...base,
3425
3599
  Fltr: {
@@ -3463,8 +3637,8 @@ interface SoLdDescriptor {
3463
3637
  'Sz ': { _name: '', _classID: 'Pnt ', Wdth: number; Hght: number; };
3464
3638
  Rslt: DescriptorUnitsValue;
3465
3639
  filterFX?: SoLdDescriptorFilter;
3466
- comp?: number; // TODO: support this ?
3467
- compInfo?: { compID: number; originalCompID: number; }; // TODO: support this ?
3640
+ comp?: number;
3641
+ compInfo?: { compID: number; originalCompID: number; };
3468
3642
  Impr?: {}; // ???
3469
3643
  ClMg?: { // TODO: support this ?
3470
3644
  _name: '';
@@ -3512,7 +3686,7 @@ function getWarpFromPlacedLayer(placed: PlacedLayer): Warp {
3512
3686
 
3513
3687
  addHandler(
3514
3688
  'SoLd',
3515
- hasKey('placedLayer'),
3689
+ target => !!target.placedLayer && !target.placedLayer.external, // linked smart objects are stored in SoLE
3516
3690
  (reader, target, left) => {
3517
3691
  if (readSignature(reader) !== 'soLD') throw new Error(`Invalid SoLd type`);
3518
3692
  const version = readInt32(reader);
@@ -3573,6 +3747,12 @@ addHandler(
3573
3747
  const placedType = placedLayerTypes.indexOf(placed.type);
3574
3748
  if (placedType === -1) throw new Error('Invalid placedLayer type');
3575
3749
 
3750
+ const warp = getWarpFromPlacedLayer(placed);
3751
+ // size of linked image matches warp bounds if it's not provided
3752
+ const bounds = warp.bounds;
3753
+ const width = placed.width || (bounds ? bounds.right.value - bounds.left.value : 0);
3754
+ const height = placed.height || (bounds ? bounds.bottom.value - bounds.top.value : 0);
3755
+
3576
3756
  const desc: SoLdDescriptor = {
3577
3757
  Idnt: placed.id,
3578
3758
  placed: placed.placed ?? placed.id,
@@ -3587,12 +3767,12 @@ addHandler(
3587
3767
  Trnf: placed.transform,
3588
3768
  nonAffineTransform: placed.nonAffineTransform ?? placed.transform,
3589
3769
  // quiltWarp: {} as any,
3590
- warp: encodeWarp(getWarpFromPlacedLayer(placed)),
3770
+ warp: encodeWarp(warp),
3591
3771
  'Sz ': {
3592
3772
  _name: '',
3593
3773
  _classID: 'Pnt ',
3594
- Wdth: placed.width || 0, // TODO: find size ?
3595
- Hght: placed.height || 0, // TODO: find size ?
3774
+ Wdth: width,
3775
+ Hght: height,
3596
3776
  },
3597
3777
  Rslt: placed.resolution ? unitsValue(placed.resolution, 'resolution') : { units: 'Density', value: 72 },
3598
3778
  };
@@ -3610,15 +3790,6 @@ addHandler(
3610
3790
  };
3611
3791
  }
3612
3792
 
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
3793
  // if (JSON.stringify(t) !== JSON.stringify(desc)) {
3623
3794
  // console.log('read', require('util').inspect(t, false, 99, true));
3624
3795
  // console.log('write', require('util').inspect(desc, false, 99, true));
@@ -3650,7 +3821,17 @@ addHandler(
3650
3821
  },
3651
3822
  );
3652
3823
 
3653
- addHandlerAlias('SoLE', 'SoLd');
3824
+ const soLdHandler = infoHandlersMap['SoLd'];
3825
+
3826
+ addHandler(
3827
+ 'SoLE', // linked smart object (Photoshop doesn't write PlLd for these)
3828
+ target => !!target.placedLayer?.external,
3829
+ (reader, target, left, psd, imageResources) => {
3830
+ soLdHandler.read(reader, target, left, psd, imageResources);
3831
+ target.placedLayer!.external = true;
3832
+ },
3833
+ soLdHandler.write,
3834
+ );
3654
3835
 
3655
3836
  addHandler(
3656
3837
  'fxrp',
@@ -3667,25 +3848,50 @@ addHandler(
3667
3848
  },
3668
3849
  );
3669
3850
 
3670
- addHandler(
3671
- 'Lr16',
3672
- () => false,
3673
- (reader, _target, _left, psd, imageResources) => {
3674
- readLayerInfo(reader, psd, imageResources);
3675
- },
3676
- (_writer, _target) => {
3677
- },
3678
- );
3851
+ function createGlobalLayerInfoHandler(key: string, bitsPerChannel: number | undefined) {
3852
+ addHandler(
3853
+ key,
3854
+ () => false,
3855
+ (reader, _target, left, psd, imageResources) => {
3856
+ // Photoshop writes empty (or 8bit compatibility) layer info section alongside Lr16/Lr32 in 16/32bit documents,
3857
+ // layers from global info with matching bit depth replace layers from that section, other ones are only used
3858
+ // if the section is empty
3859
+ if (!reader.layersInGlobalInfo && (psd.bitsPerChannel === bitsPerChannel || !psd.children?.length)) {
3860
+ const sectionLayers = psd.children;
3861
+ psd.children = [];
3862
+
3863
+ try {
3864
+ readLayerInfo(reader, psd, imageResources);
3865
+ } catch (e) {
3866
+ psd.children = sectionLayers; // keep layers from layer info section if global info is broken
3867
+ throw e;
3868
+ }
3679
3869
 
3680
- addHandler(
3681
- 'Lr32',
3682
- () => false,
3683
- (reader, _target, _left, psd, imageResources) => {
3684
- readLayerInfo(reader, psd, imageResources);
3685
- },
3686
- (_writer, _target) => {
3687
- },
3688
- );
3870
+ reader.layersInGlobalInfo = true;
3871
+ }
3872
+
3873
+ skipBytes(reader, left());
3874
+ },
3875
+ () => { },
3876
+ );
3877
+ }
3878
+
3879
+ createGlobalLayerInfoHandler('Lr16', 16);
3880
+ createGlobalLayerInfoHandler('Lr32', 32);
3881
+ createGlobalLayerInfoHandler('Layr', undefined);
3882
+
3883
+ // merged transparency markers, first extra channel of composite image is transparency
3884
+ for (const key of ['Mtrn', 'Mt16', 'Mt32']) {
3885
+ addHandler(
3886
+ key,
3887
+ () => false,
3888
+ (reader, _target, left) => {
3889
+ reader.globalAlpha = true;
3890
+ skipBytes(reader, left());
3891
+ },
3892
+ () => { },
3893
+ );
3894
+ }
3689
3895
 
3690
3896
  addHandler(
3691
3897
  'LMsk',
@@ -3720,11 +3926,12 @@ if (MOCK_HANDLERS) {
3720
3926
  );
3721
3927
  } else {
3722
3928
  addHandler(
3723
- 'Patt', // TODO: handle also Pat2 & Pat3
3929
+ 'Patt', // 'Pat2' and 'Pat3' (16 and 32 bit documents) are aliases, patterns are always written as 8 bit
3724
3930
  target => !!(target.patterns && target.patterns.length > 0),
3725
3931
  (reader, target, left) => {
3726
3932
  while (left() > 0) {
3727
- const pattern = readPattern(reader);
3933
+ const pattern = readPatternSafe(reader);
3934
+ if (!pattern) continue;
3728
3935
  if (target.patterns === undefined) target.patterns = [];
3729
3936
  target.patterns.push(pattern);
3730
3937
  }
@@ -3954,6 +4161,11 @@ interface LinkedFileDescriptor {
3954
4161
  relPath: string;
3955
4162
  }
3956
4163
 
4164
+ function readFileTypeSignature(reader: PsdReader) {
4165
+ const value = readSignature(reader);
4166
+ return /^[\0 ]*$/.test(value) ? '' : value.trim(); // empty value is stored as spaces or zeros
4167
+ }
4168
+
3957
4169
  function createLnkHandler(tag: string) {
3958
4170
  addHandler(
3959
4171
  tag,
@@ -3978,8 +4190,8 @@ function createLnkHandler(tag: string) {
3978
4190
  const id = readPascalString(reader, 1);
3979
4191
  const name = readUnicodeString(reader);
3980
4192
 
3981
- const fileType = readSignature(reader).trim(); // ' ' if empty
3982
- const fileCreator = readSignature(reader).trim(); // ' ' or '\0\0\0\0' if empty
4193
+ const fileType = readFileTypeSignature(reader); // ' ' if empty
4194
+ const fileCreator = readFileTypeSignature(reader); // ' ' or '\0\0\0\0' if empty
3983
4195
  const dataSize = readLength64(reader);
3984
4196
  const hasFileOpenDescriptor = readUint8(reader);
3985
4197
  const fileOpenDescriptor = hasFileOpenDescriptor ? readVersionAndDescriptor(reader) as FileOpenDescriptor : undefined;
@@ -4137,28 +4349,55 @@ addHandlerAlias('lnk3', 'lnk2');
4137
4349
 
4138
4350
  interface PthsDescriptor {
4139
4351
  pathList: {
4352
+ _name: '';
4140
4353
  _classID: 'pathInfoClass';
4141
4354
  pathUnicodeName: string;
4142
- pathSymmetryClass: {
4355
+ pathSymmetryClass?: {
4356
+ _name: '';
4143
4357
  _classID: 'pathSymmetryClass';
4144
4358
  pathSymmetryMode: string; // 'pathSymmetryModeEnum.pathSymmetryModeBasicPath'
4145
4359
  };
4146
4360
  }[];
4147
4361
  }
4148
4362
 
4363
+ // unicode names of saved paths (path data is stored in image resources 2000-2998)
4149
4364
  addHandler(
4150
4365
  'pths',
4151
- hasKey('pathList'),
4152
- (reader, target) => {
4366
+ target => (target as Psd).imageResources?.savedPaths !== undefined,
4367
+ (reader, _target, left, psd) => {
4153
4368
  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
4369
+ const imageResources = psd.imageResources ??= {};
4370
+ const savedPaths = imageResources.savedPaths ??= [];
4371
+
4372
+ for (let i = 0; i < desc.pathList.length && i < savedPaths.length; i++) {
4373
+ const { pathUnicodeName, pathSymmetryClass } = desc.pathList[i];
4374
+ const path = savedPaths[i];
4375
+
4376
+ // clipping path is referenced by (non-unicode) name
4377
+ if (imageResources.clippingPath && imageResources.clippingPath.name === path.name) {
4378
+ imageResources.clippingPath.name = pathUnicodeName;
4379
+ }
4380
+
4381
+ path.name = pathUnicodeName;
4382
+ if (pathSymmetryClass) path.symmetryMode = pathSymmetryClass.pathSymmetryMode.split('.')[1];
4383
+ }
4384
+
4385
+ skipBytes(reader, left());
4158
4386
  },
4159
- (writer, _target) => {
4387
+ (writer, _target, psd) => {
4160
4388
  const desc: PthsDescriptor = {
4161
- pathList: [], // TODO: write paths
4389
+ pathList: psd.imageResources!.savedPaths!.map(path => ({
4390
+ _name: '',
4391
+ _classID: 'pathInfoClass',
4392
+ pathUnicodeName: path.name,
4393
+ ...(path.symmetryMode ? {
4394
+ pathSymmetryClass: {
4395
+ _name: '',
4396
+ _classID: 'pathSymmetryClass',
4397
+ pathSymmetryMode: `pathSymmetryModeEnum.${path.symmetryMode}`,
4398
+ },
4399
+ } : {}),
4400
+ })),
4162
4401
  };
4163
4402
 
4164
4403
  writeVersionAndDescriptor(writer, '', 'pathsDataClass', desc);
@@ -4173,15 +4412,8 @@ addHandler(
4173
4412
  );
4174
4413
 
4175
4414
  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
- // ),
4415
+ 'lfxs', // effects of group layers (instead of 'lfx2' / 'lmfx')
4416
+ target => target.effects !== undefined && isGroupLayer(target),
4185
4417
  (reader, target, left) => {
4186
4418
  const version = readUint32(reader);
4187
4419
  if (version !== 0) throw new Error(`Invalid lfxs version`);
@@ -4238,6 +4470,12 @@ function readLevelsChannel(reader: PsdReader): LevelsAdjustmentChannel {
4238
4470
  return { shadowInput, highlightInput, shadowOutput, highlightOutput, midtoneInput };
4239
4471
  }
4240
4472
 
4473
+ const defaultLevelsChannel: LevelsAdjustmentChannel = { shadowInput: 0, highlightInput: 255, shadowOutput: 0, highlightOutput: 255, midtoneInput: 1 };
4474
+
4475
+ function isDefaultLevelsChannel(c: LevelsAdjustmentChannel) {
4476
+ return c.shadowInput === 0 && c.highlightInput === 255 && c.shadowOutput === 0 && c.highlightOutput === 255 && c.midtoneInput === 1;
4477
+ }
4478
+
4241
4479
  function writeLevelsChannel(writer: PsdWriter, channel: LevelsAdjustmentChannel) {
4242
4480
  writeInt16(writer, channel.shadowInput);
4243
4481
  writeInt16(writer, channel.highlightInput);
@@ -4252,62 +4490,49 @@ addHandler(
4252
4490
  (reader, target, left) => {
4253
4491
  if (readUint16(reader) !== 2) throw new Error('Invalid levl version');
4254
4492
 
4255
- const levels: LevelsAdjustmentChannel[] = [];
4493
+ const channels: LevelsAdjustmentChannel[] = [];
4256
4494
 
4257
- for (let i = 0; i < 29 && left(); i++) {
4258
- levels.push(readLevelsChannel(reader));
4495
+ for (let i = 0; i < 29 && left() >= 10; i++) {
4496
+ channels.push(readLevelsChannel(reader));
4259
4497
  }
4260
4498
 
4261
- const [rgb, red, green, blue] = levels;
4262
-
4263
- target.adjustment = {
4264
- ...target.adjustment as PresetInfo,
4265
- type: 'levels',
4266
- rgb,
4267
- red,
4268
- green,
4269
- blue,
4270
- };
4271
-
4272
- if (left()) {
4499
+ if (left() >= 8) {
4273
4500
  if (readSignature(reader) !== 'Lvls') throw new Error(`Invalid levl signature`);
4274
4501
  if (readUint16(reader) !== 3) throw new Error(`Invalid levl version`);
4275
4502
 
4276
4503
  const totalChannelRecords = readUint16(reader);
4277
4504
 
4278
- for (let i = 0; i < (totalChannelRecords - levels.length) && left(); i++) {
4279
- levels.push(readLevelsChannel(reader));
4505
+ while (channels.length < totalChannelRecords && left() >= 10) {
4506
+ channels.push(readLevelsChannel(reader));
4280
4507
  }
4281
4508
  }
4282
4509
 
4510
+ // records for all possible channels are stored, skip unused ones
4511
+ while (channels.length && isDefaultLevelsChannel(channels[channels.length - 1])) channels.pop();
4512
+
4513
+ target.adjustment = {
4514
+ ...target.adjustment as PresetInfo,
4515
+ type: 'levels',
4516
+ channels,
4517
+ };
4518
+
4283
4519
  skipBytes(reader, left());
4284
4520
  },
4285
4521
  (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
- };
4522
+ const channels = (target.adjustment as LevelsAdjustment).channels || [];
4294
4523
 
4295
4524
  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
4525
 
4301
- for (let i = 0; i < (29 - 4); i++) {
4302
- writeLevelsChannel(writer, defaultChannel);
4526
+ for (let i = 0; i < 29; i++) {
4527
+ writeLevelsChannel(writer, channels[i] || defaultLevelsChannel);
4303
4528
  }
4304
4529
 
4305
4530
  writeSignature(writer, 'Lvls');
4306
4531
  writeUint16(writer, 3); // version
4307
4532
  writeUint16(writer, 62); // total count
4308
4533
 
4309
- for (let i = 0; i < (62 - 29); i++) {
4310
- writeLevelsChannel(writer, defaultChannel);
4534
+ for (let i = 29; i < 62; i++) {
4535
+ writeLevelsChannel(writer, channels[i] || defaultLevelsChannel);
4311
4536
  }
4312
4537
  },
4313
4538
  );
@@ -4341,17 +4566,20 @@ addHandler(
4341
4566
  readUint8(reader);
4342
4567
  if (readUint16(reader) !== 1) throw new Error('Invalid curv version');
4343
4568
  readUint16(reader);
4344
- const channels = readUint16(reader);
4345
- const info: CurvesAdjustment = { type: 'curves' };
4569
+ const channelsMask = readUint16(reader);
4570
+ const channels: CurvesAdjustmentChannel[] = [];
4346
4571
 
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);
4572
+ for (let i = 0; i < 16; i++) {
4573
+ if (channelsMask & (1 << i)) {
4574
+ while (channels.length < i) channels.push([]);
4575
+ channels.push(readCurveChannel(reader));
4576
+ }
4577
+ }
4351
4578
 
4352
4579
  target.adjustment = {
4353
4580
  ...target.adjustment as PresetInfo,
4354
- ...info,
4581
+ type: 'curves',
4582
+ channels,
4355
4583
  };
4356
4584
 
4357
4585
  // ignoring, duplicate information
@@ -4374,35 +4602,33 @@ addHandler(
4374
4602
  skipBytes(reader, left());
4375
4603
  },
4376
4604
  (writer, target) => {
4377
- const info = target.adjustment as CurvesAdjustment;
4378
- const { rgb, red, green, blue } = info;
4379
- let channels = 0;
4380
- let channelCount = 0;
4381
-
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++; }
4605
+ const channels = ((target.adjustment as CurvesAdjustment).channels || []).slice(0, 16);
4606
+ const used: number[] = [];
4607
+ let channelsMask = 0;
4608
+
4609
+ for (let i = 0; i < channels.length; i++) {
4610
+ if (channels[i]?.length) {
4611
+ channelsMask |= 1 << i;
4612
+ used.push(i);
4613
+ }
4614
+ }
4386
4615
 
4387
4616
  writeUint8(writer, 0);
4388
4617
  writeUint16(writer, 1); // version
4389
4618
  writeUint16(writer, 0);
4390
- writeUint16(writer, channels);
4619
+ writeUint16(writer, channelsMask);
4391
4620
 
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);
4621
+ for (const i of used) writeCurveChannel(writer, channels[i]);
4396
4622
 
4397
4623
  writeSignature(writer, 'Crv ');
4398
4624
  writeUint16(writer, 4); // version
4399
4625
  writeUint16(writer, 0);
4400
- writeUint16(writer, channelCount);
4626
+ writeUint16(writer, used.length);
4401
4627
 
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); }
4628
+ for (const i of used) {
4629
+ writeUint16(writer, i);
4630
+ writeCurveChannel(writer, channels[i]);
4631
+ }
4406
4632
  },
4407
4633
  );
4408
4634
 
@@ -4487,10 +4713,26 @@ addHandler(
4487
4713
  (reader, target, left) => {
4488
4714
  if (readUint16(reader) !== 2) throw new Error('Invalid hue2 version');
4489
4715
 
4716
+ const colorize = !!readUint8(reader);
4717
+ skipBytes(reader, 1);
4718
+ const colorizeHue = readInt16(reader); // stored as -180..180
4719
+ const colorizeSaturation = readInt16(reader);
4720
+ const colorizeLightness = readInt16(reader);
4721
+
4490
4722
  target.adjustment = {
4491
4723
  ...target.adjustment as PresetInfo,
4492
4724
  type: 'hue/saturation',
4493
- master: readHueChannel(reader),
4725
+ colorize,
4726
+ colorization: {
4727
+ hue: colorizeHue < 0 ? colorizeHue + 360 : colorizeHue,
4728
+ saturation: colorizeSaturation,
4729
+ lightness: colorizeLightness,
4730
+ },
4731
+ master: {
4732
+ hue: readInt16(reader),
4733
+ saturation: readInt16(reader),
4734
+ lightness: readInt16(reader),
4735
+ },
4494
4736
  reds: readHueChannel(reader),
4495
4737
  yellows: readHueChannel(reader),
4496
4738
  greens: readHueChannel(reader),
@@ -4505,7 +4747,15 @@ addHandler(
4505
4747
  const info = target.adjustment as HueSaturationAdjustment;
4506
4748
 
4507
4749
  writeUint16(writer, 2); // version
4508
- writeHueChannel(writer, info.master);
4750
+ writeUint8(writer, info.colorize ? 1 : 0);
4751
+ writeUint8(writer, 0);
4752
+ const colorizeHue = Math.round(info.colorization?.hue ?? 0);
4753
+ writeInt16(writer, colorizeHue > 180 ? colorizeHue - 360 : colorizeHue);
4754
+ writeInt16(writer, Math.round(info.colorization?.saturation ?? 0));
4755
+ writeInt16(writer, Math.round(info.colorization?.lightness ?? 0));
4756
+ writeInt16(writer, Math.round(info.master?.hue ?? 0));
4757
+ writeInt16(writer, Math.round(info.master?.saturation ?? 0));
4758
+ writeInt16(writer, Math.round(info.master?.lightness ?? 0));
4509
4759
  writeHueChannel(writer, info.reds);
4510
4760
  writeHueChannel(writer, info.yellows);
4511
4761
  writeHueChannel(writer, info.greens);
@@ -4619,16 +4869,17 @@ addHandler(
4619
4869
  if (version === 2) {
4620
4870
  color = readColor(reader);
4621
4871
  } 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;
4872
+ // XYZ color relative to D50 white point, 32768 = 1.0 (not documented, matches colors of Photoshop presets),
4873
+ // converted to Lab (same scale as readColor()) so it can be written back as version 2
4874
+ const x = readInt32(reader) / 32768;
4875
+ const y = readInt32(reader) / 32768;
4876
+ const z = readInt32(reader) / 32768;
4877
+ if (!(x >= 0 && y >= 0 && z >= 0 && x <= 2 && y <= 2 && z <= 2)) throw new Error('Invalid phfl XYZ color');
4878
+ const f = (t: number) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
4628
4879
  color = {
4629
- l: l / 100,
4630
- a: normalizeLabAB(a),
4631
- b: normalizeLabAB(b),
4880
+ l: Math.max(0, 116 * f(y) - 16) / 100,
4881
+ a: normalizeLabAB(500 * (f(x) - f(y))),
4882
+ b: normalizeLabAB(200 * (f(y) - f(z))),
4632
4883
  };
4633
4884
  }
4634
4885
 
@@ -4844,7 +5095,7 @@ addHandler(
4844
5095
  const hasMethod = !!readUint8(reader); reader.offset--;
4845
5096
  if (hasMethod) {
4846
5097
  const method = readSignature(reader);
4847
- info.method = gradientInterpolationMethodType.decode(method);
5098
+ info.method = gradientInterpolationMethodType.decode(`gradientInterpolationMethodType.${method}`);
4848
5099
  }
4849
5100
 
4850
5101
  info.name = readUnicodeString(reader);
@@ -4917,7 +5168,7 @@ addHandler(
4917
5168
  writeUint8(writer, info.dither ? 1 : 0);
4918
5169
 
4919
5170
  if (info.method !== undefined) {
4920
- writeSignature(writer, gradientInterpolationMethodType.encode(info.method));
5171
+ writeSignature(writer, gradientInterpolationMethodType.encode(info.method).split('.')[1]);
4921
5172
  }
4922
5173
 
4923
5174
  writeUnicodeStringWithPadding(writer, info.name || '');
@@ -5334,14 +5585,14 @@ addHandler(
5334
5585
 
5335
5586
  interface ArtdDescriptor {
5336
5587
  'Cnt ': number;
5337
- autoExpandOffset: { Hrzn: number; Vrtc: number; };
5338
- origin: { Hrzn: number; Vrtc: number; };
5339
- autoExpandEnabled: boolean;
5340
- autoNestEnabled: boolean;
5341
- autoPositionEnabled: boolean;
5588
+ autoExpandOffset?: { Hrzn: number; Vrtc: number; };
5589
+ origin?: { Hrzn: number; Vrtc: number; };
5590
+ autoExpandEnabled?: boolean;
5591
+ autoNestEnabled?: boolean;
5592
+ autoPositionEnabled?: boolean;
5342
5593
  shrinkwrapOnSaveEnabled?: boolean;
5343
- docDefaultNewArtboardBackgroundColor: DescriptorColor;
5344
- docDefaultNewArtboardBackgroundType: number;
5594
+ docDefaultNewArtboardBackgroundColor?: DescriptorColor;
5595
+ docDefaultNewArtboardBackgroundType?: number;
5345
5596
  }
5346
5597
 
5347
5598
  addHandler(
@@ -5349,17 +5600,15 @@ addHandler(
5349
5600
  target => (target as Psd).artboards !== undefined,
5350
5601
  (reader, target, left) => {
5351
5602
  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
- };
5603
+ const artboards: Psd['artboards'] = (target as Psd).artboards = { count: desc['Cnt '] };
5604
+ if (desc.autoExpandOffset) artboards.autoExpandOffset = { horizontal: desc.autoExpandOffset.Hrzn, vertical: desc.autoExpandOffset.Vrtc };
5605
+ if (desc.origin) artboards.origin = { horizontal: desc.origin.Hrzn, vertical: desc.origin.Vrtc };
5606
+ if (desc.autoExpandEnabled !== undefined) artboards.autoExpandEnabled = desc.autoExpandEnabled;
5607
+ if (desc.autoNestEnabled !== undefined) artboards.autoNestEnabled = desc.autoNestEnabled;
5608
+ if (desc.autoPositionEnabled !== undefined) artboards.autoPositionEnabled = desc.autoPositionEnabled;
5609
+ artboards.shrinkwrapOnSaveEnabled = !!desc.shrinkwrapOnSaveEnabled;
5610
+ if (desc.docDefaultNewArtboardBackgroundColor) artboards.docDefaultNewArtboardBackgroundColor = parseColor(desc.docDefaultNewArtboardBackgroundColor);
5611
+ if (desc.docDefaultNewArtboardBackgroundType !== undefined) artboards.docDefaultNewArtboardBackgroundType = desc.docDefaultNewArtboardBackgroundType;
5363
5612
 
5364
5613
  skipBytes(reader, left());
5365
5614
  },
@@ -5380,13 +5629,17 @@ addHandler(
5380
5629
  },
5381
5630
  );
5382
5631
 
5632
+ function isGroupLayer(target: LayerAdditionalInfo) {
5633
+ return !!(target as Layer).children;
5634
+ }
5635
+
5383
5636
  export function hasMultiEffects(effects: LayerEffectsInfo) {
5384
5637
  return Object.keys(effects).map(key => (effects as any)[key]).some(v => Array.isArray(v) && v.length > 1);
5385
5638
  }
5386
5639
 
5387
5640
  addHandler(
5388
5641
  'lfx2',
5389
- target => target.effects !== undefined && !hasMultiEffects(target.effects),
5642
+ target => target.effects !== undefined && !isGroupLayer(target) && !hasMultiEffects(target.effects),
5390
5643
  (reader, target, left) => {
5391
5644
  const version = readUint32(reader);
5392
5645
  if (version !== 0) throw new Error(`Invalid lfx2 version`);
@@ -5394,8 +5647,7 @@ addHandler(
5394
5647
  const desc: Lfx2Descriptor & LmfxDescriptor = readVersionAndDescriptor(reader);
5395
5648
  // console.log('READ', require('util').inspect(desc, false, 99, true));
5396
5649
 
5397
- // TODO: don't discard if we got it from lmfx
5398
- // discard if read in 'lrFX' section
5650
+ // discard if read in 'lrFX' section ('lmfx' and 'lfx2' are never stored together)
5399
5651
  target.effects = parseEffects(desc, !!reader.logMissingFeatures);
5400
5652
 
5401
5653
  skipBytes(reader, left());
@@ -5469,26 +5721,17 @@ addHandler(
5469
5721
  },
5470
5722
  );
5471
5723
 
5472
- interface ExtensionDesc {
5473
- generatorSettings: {
5474
- generator_45_assets: { json: string; };
5475
- layerTime: number;
5476
- };
5477
- }
5478
-
5479
- // extension settings ?, ignore it
5724
+ // generator settings of the document (layers store them in 'shmd' section)
5480
5725
  addHandler(
5481
5726
  '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));
5486
-
5487
- if (MOCK_HANDLERS) (target as any)._extn = desc;
5727
+ target => target.generatorSettings !== undefined && isDocument(target),
5728
+ (reader, target, left) => {
5729
+ const desc = readVersionAndDescriptor(reader) as GeneratorSettingsDescriptor;
5730
+ if (desc.generatorSettings) target.generatorSettings = parseGeneratorSettings(desc.generatorSettings);
5731
+ skipBytes(reader, left());
5488
5732
  },
5489
5733
  (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);
5734
+ writeVersionAndDescriptor(writer, '', 'null', serializeGeneratorSettings(target.generatorSettings!));
5492
5735
  },
5493
5736
  );
5494
5737