ag-psd 31.0.3 → 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 (159) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/00d0e0b4 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/0cc76730 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/20729831 +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/214a593b +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/25f8b84a +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/27518289 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/330ef2d9 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/37ff16b8 +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/3f72a97c +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/75efccd4 +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/762f9305 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/7bcfc50a +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  22. package/.v8-cache/v24.7.0-x64-ef5a0af0/825e9a05 +0 -0
  23. package/.v8-cache/v24.7.0-x64-ef5a0af0/87372817 +0 -0
  24. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  25. package/.v8-cache/v24.7.0-x64-ef5a0af0/891a7c73 +0 -0
  26. package/.v8-cache/v24.7.0-x64-ef5a0af0/8b68a3a2 +0 -0
  27. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  28. package/.v8-cache/v24.7.0-x64-ef5a0af0/a1d12a93 +0 -0
  29. package/.v8-cache/v24.7.0-x64-ef5a0af0/a7e80865 +0 -0
  30. package/.v8-cache/v24.7.0-x64-ef5a0af0/ac317c9c +0 -0
  31. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  32. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  33. package/.v8-cache/v24.7.0-x64-ef5a0af0/b8569d6b +0 -0
  34. package/.v8-cache/v24.7.0-x64-ef5a0af0/b8b073c4 +0 -0
  35. package/.v8-cache/v24.7.0-x64-ef5a0af0/bb489c89 +0 -0
  36. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  37. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  38. package/.v8-cache/v24.7.0-x64-ef5a0af0/c46725e1 +0 -0
  39. package/.v8-cache/v24.7.0-x64-ef5a0af0/c794cb34 +0 -0
  40. package/.v8-cache/v24.7.0-x64-ef5a0af0/cd4ae980 +0 -0
  41. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  42. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  43. package/.v8-cache/v24.7.0-x64-ef5a0af0/d02dd6f7 +0 -0
  44. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  45. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  46. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6735171 +0 -0
  47. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  48. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  49. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  50. package/.v8-cache/v24.7.0-x64-ef5a0af0/f5417276 +0 -0
  51. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  52. package/.v8-cache/v24.7.0-x64-ef5a0af0/f74d3518 +0 -0
  53. package/.v8-cache/v24.7.0-x64-ef5a0af0/f886deac +0 -0
  54. package/.v8-cache/v24.7.0-x64-ef5a0af0/fd0a2987 +0 -0
  55. package/.v8-cache/v24.7.0-x64-ef5a0af0/fe349423 +0 -0
  56. package/CHANGELOG.md +107 -0
  57. package/LICENSE +5 -0
  58. package/README.md +3 -6
  59. package/README_PSD.md +242 -81
  60. package/dist/abr.d.ts +11 -4
  61. package/dist/abr.js +363 -229
  62. package/dist/abr.js.map +1 -1
  63. package/dist/additionalInfo.d.ts +56 -2
  64. package/dist/additionalInfo.js +600 -364
  65. package/dist/additionalInfo.js.map +1 -1
  66. package/dist/ase.d.ts +6 -1
  67. package/dist/ase.js +61 -61
  68. package/dist/ase.js.map +1 -1
  69. package/dist/bundle.js +3096 -1541
  70. package/dist/descriptor.d.ts +25 -6
  71. package/dist/descriptor.js +380 -135
  72. package/dist/descriptor.js.map +1 -1
  73. package/dist/effectsHelpers.js +12 -12
  74. package/dist/effectsHelpers.js.map +1 -1
  75. package/dist/engineData.d.ts +3 -1
  76. package/dist/engineData.js +34 -13
  77. package/dist/engineData.js.map +1 -1
  78. package/dist/engineData2.js +50 -21
  79. package/dist/engineData2.js.map +1 -1
  80. package/dist/helpers.d.ts +6 -3
  81. package/dist/helpers.js +22 -6
  82. package/dist/helpers.js.map +1 -1
  83. package/dist/imageResources.d.ts +15 -3
  84. package/dist/imageResources.js +529 -144
  85. package/dist/imageResources.js.map +1 -1
  86. package/dist/index.js +1 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/jpeg.js +30 -10
  89. package/dist/jpeg.js.map +1 -1
  90. package/dist/psd.d.ts +154 -18
  91. package/dist/psd.js.map +1 -1
  92. package/dist/psdReader.d.ts +12 -2
  93. package/dist/psdReader.js +720 -421
  94. package/dist/psdReader.js.map +1 -1
  95. package/dist/psdWriter.d.ts +3 -0
  96. package/dist/psdWriter.js +167 -130
  97. package/dist/psdWriter.js.map +1 -1
  98. package/dist/text.js +143 -29
  99. package/dist/text.js.map +1 -1
  100. package/dist/utf8.js +36 -18
  101. package/dist/utf8.js.map +1 -1
  102. package/dist-es/abr.d.ts +11 -4
  103. package/dist-es/abr.js +363 -229
  104. package/dist-es/abr.js.map +1 -1
  105. package/dist-es/additionalInfo.d.ts +56 -2
  106. package/dist-es/additionalInfo.js +600 -364
  107. package/dist-es/additionalInfo.js.map +1 -1
  108. package/dist-es/ase.d.ts +6 -1
  109. package/dist-es/ase.js +61 -61
  110. package/dist-es/ase.js.map +1 -1
  111. package/dist-es/descriptor.d.ts +25 -6
  112. package/dist-es/descriptor.js +380 -135
  113. package/dist-es/descriptor.js.map +1 -1
  114. package/dist-es/effectsHelpers.js +12 -12
  115. package/dist-es/effectsHelpers.js.map +1 -1
  116. package/dist-es/engineData.d.ts +3 -1
  117. package/dist-es/engineData.js +34 -13
  118. package/dist-es/engineData.js.map +1 -1
  119. package/dist-es/engineData2.js +50 -21
  120. package/dist-es/engineData2.js.map +1 -1
  121. package/dist-es/helpers.d.ts +6 -3
  122. package/dist-es/helpers.js +22 -6
  123. package/dist-es/helpers.js.map +1 -1
  124. package/dist-es/imageResources.d.ts +15 -3
  125. package/dist-es/imageResources.js +529 -144
  126. package/dist-es/imageResources.js.map +1 -1
  127. package/dist-es/index.js +1 -0
  128. package/dist-es/index.js.map +1 -1
  129. package/dist-es/jpeg.js +30 -10
  130. package/dist-es/jpeg.js.map +1 -1
  131. package/dist-es/psd.d.ts +154 -18
  132. package/dist-es/psd.js.map +1 -1
  133. package/dist-es/psdReader.d.ts +12 -2
  134. package/dist-es/psdReader.js +720 -421
  135. package/dist-es/psdReader.js.map +1 -1
  136. package/dist-es/psdWriter.d.ts +3 -0
  137. package/dist-es/psdWriter.js +167 -130
  138. package/dist-es/psdWriter.js.map +1 -1
  139. package/dist-es/text.js +143 -29
  140. package/dist-es/text.js.map +1 -1
  141. package/dist-es/utf8.js +36 -18
  142. package/dist-es/utf8.js.map +1 -1
  143. package/package.json +2 -2
  144. package/src/abr.ts +380 -232
  145. package/src/additionalInfo.ts +781 -477
  146. package/src/ase.ts +66 -61
  147. package/src/descriptor.ts +386 -142
  148. package/src/effectsHelpers.ts +9 -9
  149. package/src/engineData.ts +36 -13
  150. package/src/engineData2.ts +468 -432
  151. package/src/helpers.ts +23 -5
  152. package/src/imageResources.ts +658 -167
  153. package/src/index.ts +1 -0
  154. package/src/jpeg.ts +27 -12
  155. package/src/psd.ts +158 -30
  156. package/src/psdReader.ts +732 -414
  157. package/src/psdWriter.ts +186 -132
  158. package/src/text.ts +139 -18
  159. package/src/utf8.ts +35 -21
@@ -1,9 +1,9 @@
1
1
  import { fromByteArray, toByteArray } from 'base64-js';
2
2
  import { readEffects, writeEffects } from './effectsHelpers';
3
- import { clamp, createEnum, layerColors, MOCK_HANDLERS } 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';
6
- import { PsdWriter, writeZeros, writeSignature, writeBytes, writeUint32, writeUint16, writeFloat64, writeUint8, writeInt16, writeFloat32, writeFixedPointPath32, writeUnicodeString, writeSection, writeUnicodeStringWithPadding, writeColor, writePascalString, writeInt32, writePattern } from './psdWriter';
3
+ import { clamp, createEnum, layerColors, MOCK_HANDLERS, normalizeLabAB } from './helpers';
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
+ 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';
9
9
  import { encodeEngineData, decodeEngineData } from './text';
@@ -13,6 +13,8 @@ import type { InternalImageResources } from './imageResources';
13
13
  export interface ExtendedWriteOptions extends WriteOptions {
14
14
  layerIds: Set<number>;
15
15
  layerToId: Map<any, number>;
16
+ explicitLayerIds?: Set<number>; // ids set on layers in the document (generated ids can't use them)
17
+ nextGeneratedLayerId?: number; // all ids below this one are already taken
16
18
  }
17
19
 
18
20
  type HasMethod = (target: LayerAdditionalInfo) => boolean;
@@ -49,11 +51,6 @@ function readLength64(reader: PsdReader) {
49
51
  return readUint32(reader);
50
52
  }
51
53
 
52
- function writeLength64(writer: PsdWriter, length: number) {
53
- writeUint32(writer, 0);
54
- writeUint32(writer, length);
55
- }
56
-
57
54
  addHandler(
58
55
  'TySh',
59
56
  hasKey('text'),
@@ -95,7 +92,7 @@ addHandler(
95
92
  if (text.boundingBox) target.text.boundingBox = descBoundsToBounds(text.boundingBox);
96
93
 
97
94
  if (text.EngineData) {
98
- const engineData = parseEngineData(text.EngineData);
95
+ const engineData = parseEngineData(text.EngineData, reader);
99
96
  const textData = decodeEngineData(engineData);
100
97
  // console.log(require('util').inspect(engineData, false, 99, false), 'utf8');
101
98
 
@@ -106,7 +103,8 @@ addHandler(
106
103
  // require('fs').writeFileSync('after.txt', require('util').inspect(after, false, 99, false), 'utf8');
107
104
 
108
105
  // console.log(require('util').inspect(parseEngineData(text.EngineData), false, 99, true));
109
- 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 };
110
108
  // console.log(require('util').inspect(target.text, false, 99, true));
111
109
  }
112
110
 
@@ -140,10 +138,10 @@ addHandler(
140
138
  writeInt16(writer, 1); // warp version
141
139
  writeVersionAndDescriptor(writer, '', 'warp', encodeWarp(warp));
142
140
 
143
- writeFloat32(writer, text.left!);
144
- writeFloat32(writer, text.top!);
145
- writeFloat32(writer, text.right!);
146
- writeFloat32(writer, text.bottom!);
141
+ writeFloat32(writer, text.left ?? 0);
142
+ writeFloat32(writer, text.top ?? 0);
143
+ writeFloat32(writer, text.right ?? 0);
144
+ writeFloat32(writer, text.bottom ?? 0);
147
145
 
148
146
  // writeZeros(writer, 2);
149
147
  },
@@ -231,7 +229,7 @@ function writeBezierKnot(writer: PsdWriter, points: number[], width: number, hei
231
229
 
232
230
  export const booleanOperations: BooleanOperation[] = ['exclude', 'combine', 'subtract', 'intersect'];
233
231
 
234
- 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) {
235
233
  const end = reader.offset + size;
236
234
  const paths = vectorMask.paths;
237
235
  let path: BezierPath | undefined = undefined;
@@ -286,6 +284,51 @@ export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, w
286
284
  return paths;
287
285
  }
288
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
+
289
332
  addHandler(
290
333
  'vmsk',
291
334
  hasKey('vectorMask'),
@@ -315,45 +358,11 @@ addHandler(
315
358
 
316
359
  writeUint32(writer, 3); // version
317
360
  writeUint32(writer, flags);
318
-
319
- // initial entry
320
- writeUint16(writer, 6);
321
- writeZeros(writer, 24);
322
-
323
- const clipboard = vectorMask.clipboard;
324
- if (clipboard) {
325
- writeUint16(writer, 7);
326
- writeFixedPointPath32(writer, clipboard.top);
327
- writeFixedPointPath32(writer, clipboard.left);
328
- writeFixedPointPath32(writer, clipboard.bottom);
329
- writeFixedPointPath32(writer, clipboard.right);
330
- writeFixedPointPath32(writer, clipboard.resolution);
331
- writeZeros(writer, 4);
332
- }
333
-
334
- writeUint16(writer, 8);
335
- writeUint16(writer, vectorMask.fillStartsWithAllPixels ? 1 : 0);
336
- writeZeros(writer, 22);
337
-
338
- for (const path of vectorMask.paths) {
339
- writeUint16(writer, path.open ? 3 : 0);
340
- writeUint16(writer, path.knots.length);
341
- writeUint16(writer, path.operation ? booleanOperations.indexOf(path.operation) : -1); // -1 for undefined
342
- writeUint16(writer, path.fillRule === 'non-zero' ? 2 : 1);
343
- writeZeros(writer, 18); // TODO: these are sometimes non-zero
344
-
345
- const linkedKnot = path.open ? 4 : 1;
346
- const unlinkedKnot = path.open ? 5 : 2;
347
-
348
- for (const { linked, points } of path.knots) {
349
- writeUint16(writer, linked ? linkedKnot : unlinkedKnot);
350
- writeBezierKnot(writer, points, width, height);
351
- }
352
- }
361
+ writeVectorMask(writer, vectorMask, width, height, true);
353
362
  },
354
363
  );
355
364
 
356
- // TODO: need to write vmsk if has outline ?
365
+ // written instead of 'vmsk' for layers with vector stroke ('vscg' section)
357
366
  addHandlerAlias('vsms', 'vmsk');
358
367
  // addHandlerAlias('vmsk', 'vsms');
359
368
 
@@ -368,7 +377,7 @@ addHandler(
368
377
  },
369
378
  );
370
379
 
371
- interface VogkDescriptor {
380
+ export interface KeyDescriptorListDescriptor {
372
381
  keyDescriptorList: {
373
382
  keyShapeInvalidated?: boolean;
374
383
  keyOriginType?: number;
@@ -398,121 +407,128 @@ interface VogkDescriptor {
398
407
  }[];
399
408
  }
400
409
 
401
- addHandler(
402
- 'vogk',
403
- hasKey('vectorOrigination'),
404
- (reader, target, left) => {
405
- if (readInt32(reader) !== 1) throw new Error(`Invalid vogk version`);
406
- const desc = readVersionAndDescriptor(reader) as VogkDescriptor;
407
- // console.log(require('util').inspect(desc, false, 99, true));
408
-
409
- 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
+ }
410
449
 
411
- for (const i of desc.keyDescriptorList) {
412
- const item: KeyDescriptorItem = {};
450
+ keyDescriptorList.push(item);
451
+ }
413
452
 
414
- if (i.keyShapeInvalidated != null) item.keyShapeInvalidated = i.keyShapeInvalidated;
415
- if (i.keyOriginType != null) item.keyOriginType = i.keyOriginType;
416
- if (i.keyOriginResolution != null) item.keyOriginResolution = i.keyOriginResolution;
417
- if (i.keyOriginShapeBBox) {
418
- item.keyOriginShapeBoundingBox = {
419
- top: parseUnitsOrNumber(i.keyOriginShapeBBox['Top ']),
420
- left: parseUnitsOrNumber(i.keyOriginShapeBBox.Left),
421
- bottom: parseUnitsOrNumber(i.keyOriginShapeBBox.Btom),
422
- right: parseUnitsOrNumber(i.keyOriginShapeBBox.Rght),
423
- };
424
- }
425
- const rectRadii = i.keyOriginRRectRadii;
426
- if (rectRadii) {
427
- item.keyOriginRRectRadii = {
428
- topRight: parseUnits(rectRadii.topRight),
429
- topLeft: parseUnits(rectRadii.topLeft),
430
- bottomLeft: parseUnits(rectRadii.bottomLeft),
431
- bottomRight: parseUnits(rectRadii.bottomRight),
432
- };
433
- }
434
- const corners = i.keyOriginBoxCorners;
435
- if (corners) {
436
- item.keyOriginBoxCorners = [
437
- { x: corners.rectangleCornerA.Hrzn, y: corners.rectangleCornerA.Vrtc },
438
- { x: corners.rectangleCornerB.Hrzn, y: corners.rectangleCornerB.Vrtc },
439
- { x: corners.rectangleCornerC.Hrzn, y: corners.rectangleCornerC.Vrtc },
440
- { x: corners.rectangleCornerD.Hrzn, y: corners.rectangleCornerD.Vrtc },
441
- ];
442
- }
443
- const trnf = i.Trnf;
444
- if (trnf) {
445
- item.transform = [trnf.xx, trnf.xy, trnf.yx, trnf.yy, trnf.tx, trnf.ty];
446
- }
453
+ return keyDescriptorList;
454
+ }
447
455
 
448
- target.vectorOrigination.keyDescriptorList.push(item);
449
- }
456
+ export function serializeKeyDescriptorList(keyDescriptorList: KeyDescriptorItem[]) {
457
+ const desc: KeyDescriptorListDescriptor = { keyDescriptorList: [] };
450
458
 
451
- skipBytes(reader, left());
452
- },
453
- (writer, target) => {
454
- target;
455
- const orig = target.vectorOrigination!;
456
- const desc: VogkDescriptor = { keyDescriptorList: [] };
459
+ for (let i = 0; i < keyDescriptorList.length; i++) {
460
+ const item = keyDescriptorList[i];
457
461
 
458
- for (let i = 0; i < orig.keyDescriptorList.length; i++) {
459
- const item = orig.keyDescriptorList[i];
462
+ desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
460
463
 
461
- desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end
464
+ const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1];
462
465
 
463
- 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;
464
468
 
465
- if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType;
466
- 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
+ }
467
479
 
468
- const radii = item.keyOriginRRectRadii;
469
- if (radii) {
470
- out.keyOriginRRectRadii = {
471
- unitValueQuadVersion: 1,
472
- topRight: unitsValue(radii.topRight, 'topRight'),
473
- topLeft: unitsValue(radii.topLeft, 'topLeft'),
474
- bottomLeft: unitsValue(radii.bottomLeft, 'bottomLeft'),
475
- bottomRight: unitsValue(radii.bottomRight, 'bottomRight'),
476
- };
477
- }
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
+ }
478
490
 
479
- const box = item.keyOriginShapeBoundingBox;
480
- if (box) {
481
- out.keyOriginShapeBBox = {
482
- unitValueQuadVersion: 1,
483
- 'Top ': unitsValue(box.top, 'top'),
484
- Left: unitsValue(box.left, 'left'),
485
- Btom: unitsValue(box.bottom, 'bottom'),
486
- Rght: unitsValue(box.right, 'right'),
487
- };
488
- }
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
+ }
489
500
 
490
- const corners = item.keyOriginBoxCorners;
491
- if (corners && corners.length === 4) {
492
- out.keyOriginBoxCorners = {
493
- rectangleCornerA: { Hrzn: corners[0].x, Vrtc: corners[0].y },
494
- rectangleCornerB: { Hrzn: corners[1].x, Vrtc: corners[1].y },
495
- rectangleCornerC: { Hrzn: corners[2].x, Vrtc: corners[2].y },
496
- rectangleCornerD: { Hrzn: corners[3].x, Vrtc: corners[3].y },
497
- };
498
- }
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
+ }
499
512
 
500
- const transform = item.transform;
501
- if (transform && transform.length === 6) {
502
- out.Trnf = {
503
- xx: transform[0],
504
- xy: transform[1],
505
- yx: transform[2],
506
- yy: transform[3],
507
- tx: transform[4],
508
- ty: transform[5],
509
- };
510
- }
513
+ if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated;
514
+ out.keyOriginIndex = i;
515
+ }
511
516
 
512
- if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated;
513
- out.keyOriginIndex = i;
514
- }
517
+ return desc;
518
+ }
515
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);
516
532
  writeInt32(writer, 1); // version
517
533
  writeVersionAndDescriptor(writer, '', 'null', desc);
518
534
  }
@@ -520,7 +536,7 @@ addHandler(
520
536
 
521
537
  addHandler(
522
538
  'lmfx',
523
- target => target.effects !== undefined && hasMultiEffects(target.effects),
539
+ target => target.effects !== undefined && !isGroupLayer(target) && hasMultiEffects(target.effects),
524
540
  (reader, target, left) => {
525
541
  const version = readUint32(reader);
526
542
  if (version !== 0) throw new Error('Invalid lmfx version');
@@ -586,15 +602,58 @@ addHandler(
586
602
  (writer, target) => writeSignature(writer, target.nameSource!),
587
603
  );
588
604
 
605
+ // these sections reference layer id
606
+ function layerNeedsId(target: LayerAdditionalInfo) {
607
+ return !!(target.animationFrames || target.timeline || target.comps);
608
+ }
609
+
610
+ function getExplicitLayerIds(psd: Psd, options: ExtendedWriteOptions) {
611
+ if (!options.explicitLayerIds) {
612
+ const ids = new Set<number>();
613
+ const addIds = (layers: Layer[] | undefined) => {
614
+ for (const layer of layers ?? []) {
615
+ if (layer.id !== undefined) ids.add(layer.id);
616
+ addIds(layer.children);
617
+ }
618
+ };
619
+ addIds(psd.children);
620
+ options.explicitLayerIds = ids;
621
+ }
622
+
623
+ return options.explicitLayerIds;
624
+ }
625
+
626
+ function getLayerId(target: LayerAdditionalInfo, options: ExtendedWriteOptions) {
627
+ const id = options.layerToId.get(target) ?? target.id;
628
+ if (id === undefined) throw new Error('Missing layer id');
629
+ return id;
630
+ }
631
+
589
632
  addHandler(
590
633
  'lyid',
591
- hasKey('id'),
634
+ target => target.id !== undefined || layerNeedsId(target),
592
635
  (reader, target) => {
593
636
  target.id = readUint32(reader);
594
637
  },
595
- (writer, target, _psd, options) => {
596
- let id = target.id!;
597
- while (options.layerIds.has(id)) id += 100; // make sure we don't have duplicate layer ids
638
+ (writer, target, psd, options) => {
639
+ let id: number;
640
+
641
+ if (target.id !== undefined) {
642
+ id = target.id;
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
+ }
649
+ } else {
650
+ // generate id for layers that don't have it, if it's referenced by other sections
651
+ const explicitIds = getExplicitLayerIds(psd, options);
652
+ id = options.nextGeneratedLayerId ?? 1;
653
+ while (options.layerIds.has(id) || explicitIds.has(id)) id++;
654
+ options.nextGeneratedLayerId = id + 1;
655
+ }
656
+
598
657
  writeUint32(writer, id);
599
658
  options.layerIds.add(id);
600
659
  options.layerToId.set(target, id);
@@ -698,15 +757,17 @@ addHandler(
698
757
  };
699
758
 
700
759
  if (flags & 0x08) target.protected.artboards = true;
760
+ if (flags & 0x80000000) target.protected.all = true;
701
761
  },
702
762
  (writer, target) => {
703
763
  const flags =
704
764
  (target.protected!.transparency ? 0x01 : 0) |
705
765
  (target.protected!.composite ? 0x02 : 0) |
706
766
  (target.protected!.position ? 0x04 : 0) |
707
- (target.protected!.artboards ? 0x08 : 0);
767
+ (target.protected!.artboards ? 0x08 : 0) |
768
+ (target.protected!.all ? 0x80000000 : 0);
708
769
 
709
- writeUint32(writer, flags);
770
+ writeUint32(writer, flags >>> 0);
710
771
  },
711
772
  );
712
773
 
@@ -729,6 +790,62 @@ interface CustomDescriptor {
729
790
  layerTime?: number;
730
791
  }
731
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
+
732
849
  interface CmlsDescriptor {
733
850
  origFXRefPoint?: { Hrzn: number; Vrtc: number; };
734
851
  LyrI: number;
@@ -742,112 +859,113 @@ interface CmlsDescriptor {
742
859
 
743
860
  addHandler(
744
861
  'shmd', // Metadata setting
745
- 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)),
746
864
  (reader, target, left) => {
747
865
  const count = readUint32(reader);
748
866
 
749
867
  for (let i = 0; i < count; i++) {
750
- 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
751
870
  const key = readSignature(reader);
752
871
  readUint8(reader); // copy
753
872
  skipBytes(reader, 3);
754
873
 
755
- readSection(reader, 1, left => {
756
- if (key === 'cust') {
757
- const desc = readVersionAndDescriptor(reader) as CustomDescriptor;
758
- // console.log('cust', target.name, require('util').inspect(desc, false, 99, true));
759
- if (desc.layerTime !== undefined) target.timestamp = desc.layerTime;
760
- } else if (key === 'mlst') {
761
- const desc = readVersionAndDescriptor(reader) as FrameListDescriptor;
762
- // console.log('mlst', target.name, require('util').inspect(desc, false, 99, true));
763
-
764
- target.animationFrames = [];
765
-
766
- for (let i = 0; i < desc.LaSt.length; i++) {
767
- const f = desc.LaSt[i];
768
- const frame: AnimationFrame = { frames: f.FrLs };
769
- if (f.enab !== undefined) frame.enable = f.enab;
770
- if (f.Ofst) frame.offset = horzVrtcToXY(f.Ofst);
771
- if (f.FXRf) frame.referencePoint = horzVrtcToXY(f.FXRf);
772
- if (f.Lefx) frame.effects = parseEffects(f.Lefx, !!reader.logMissingFeatures);
773
- if (f.blendOptions && f.blendOptions.Opct) frame.opacity = parsePercent(f.blendOptions.Opct);
774
- target.animationFrames.push(frame);
775
- }
776
- } else if (key === 'mdyn') {
777
- // frame flags
778
- readUint16(reader); // unknown
779
- const propagate = readUint8(reader);
780
- const flags = readUint8(reader);
781
-
782
- target.animationFrameFlags = {
783
- propagateFrameOne: !propagate,
784
- unifyLayerPosition: (flags & 1) !== 0,
785
- unifyLayerStyle: (flags & 2) !== 0,
786
- unifyLayerVisibility: (flags & 4) !== 0,
787
- };
788
- } else if (key === 'tmln') {
789
- const desc = readVersionAndDescriptor(reader) as TimelineDescriptor;
790
- const timeScope = desc.timeScope;
791
- // console.log('tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
792
-
793
- const timeline: Timeline = {
794
- start: frac(timeScope.Strt),
795
- duration: frac(timeScope.duration),
796
- inTime: frac(timeScope.inTime),
797
- outTime: frac(timeScope.outTime),
798
- autoScope: desc.autoScope,
799
- audioLevel: desc.audioLevel,
800
- };
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
+ };
801
923
 
802
- if (desc.trackList) {
803
- timeline.tracks = parseTrackList(desc.trackList, !!reader.logMissingFeatures);
804
- }
924
+ if (desc.trackList) {
925
+ timeline.tracks = parseTrackList(desc.trackList, !!reader.logMissingFeatures);
926
+ }
805
927
 
806
- target.timeline = timeline;
807
- // console.log('tmln:result', target.name, target.id, require('util').inspect(timeline, false, 99, true));
808
- } else if (key === 'cmls') {
809
- const desc = readVersionAndDescriptor(reader) as CmlsDescriptor;
810
- // 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));
811
933
 
812
- target.comps = {
813
- settings: [],
814
- };
934
+ target.comps = {
935
+ settings: [],
936
+ };
815
937
 
816
- 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 };
817
939
 
818
- for (const item of desc.layerSettings) {
819
- target.comps.settings.push({ compList: item.compList });
820
- const t = target.comps.settings[target.comps.settings.length - 1];
821
- if ('enab' in item) t.enabled = item.enab;
822
- if (item.Ofst) t.offset = { x: item.Ofst.Hrzn, y: item.Ofst.Vrtc };
823
- if (item.FXRefPoint) t.effectsReferencePoint = { x: item.FXRefPoint.Hrzn, y: item.FXRefPoint.Vrtc };
824
- }
825
- } else if (key === 'extn') {
826
- interface ExtnDescriptor {
827
- generatorSettings: {
828
- exportAs: {
829
- exportOption: string;
830
- };
831
- layerTime: number;
832
- };
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);
833
952
  }
834
953
 
835
- const desc = readVersionAndDescriptor(reader) as ExtnDescriptor;
836
- // console.log(require('util').inspect(desc, false, 99, true));
837
- desc; // TODO: save this
838
- reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key);
839
- } else {
840
- reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key);
841
- }
842
-
843
- skipBytes(reader, left());
844
- });
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
+ }
845
962
  }
846
963
 
847
964
  skipBytes(reader, left());
848
965
  },
849
966
  (writer, target, _, options) => {
850
967
  const { animationFrames, animationFrameFlags, timestamp, timeline, comps } = target;
968
+ const generatorSettings = isDocument(target) ? undefined : target.generatorSettings;
851
969
 
852
970
  let count = 0;
853
971
  if (animationFrames) count++;
@@ -855,6 +973,7 @@ addHandler(
855
973
  if (timeline) count++;
856
974
  if (timestamp !== undefined) count++;
857
975
  if (comps) count++;
976
+ if (generatorSettings) count++;
858
977
  writeUint32(writer, count);
859
978
 
860
979
  if (animationFrames) {
@@ -864,7 +983,7 @@ addHandler(
864
983
  writeZeros(writer, 3);
865
984
  writeSection(writer, 2, () => {
866
985
  const desc: FrameListDescriptor = {
867
- LaID: target.id ?? 0,
986
+ LaID: getLayerId(target, options),
868
987
  LaSt: [],
869
988
  };
870
989
 
@@ -875,8 +994,8 @@ addHandler(
875
994
  frame.FrLs = f.frames;
876
995
  if (f.offset) frame.Ofst = xyToHorzVrtc(f.offset);
877
996
  if (f.referencePoint) frame.FXRf = xyToHorzVrtc(f.referencePoint);
878
- if (f.effects) frame.Lefx = serializeEffects(f.effects, false, false);
879
- 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) };
880
999
  desc.LaSt.push(frame);
881
1000
  }
882
1001
 
@@ -922,9 +1041,7 @@ addHandler(
922
1041
  desc.trackList = serializeTrackList(timeline.tracks);
923
1042
  }
924
1043
 
925
- const id = options.layerToId.get(target) || target.id;
926
- if (!id) throw new Error('You need to provide layer.id value whan writing document with animations');
927
- desc.LyrI = id;
1044
+ desc.LyrI = getLayerId(target, options);
928
1045
 
929
1046
  // console.log('WRITE:tmln', target.name, target.id, require('util').inspect(desc, false, 99, true));
930
1047
  writeVersionAndDescriptor(writer, '', 'null', desc, 'anim');
@@ -950,16 +1067,13 @@ addHandler(
950
1067
  writeUint8(writer, 0); // copy (always false)
951
1068
  writeZeros(writer, 3);
952
1069
  writeSection(writer, 2, () => {
953
- const id = options.layerToId.get(target) || target.id;
954
- if (!id) throw new Error('You need to provide layer.id value whan writing document with layer comps');
955
-
956
1070
  const desc: CmlsDescriptor = {} as any;
957
1071
 
958
1072
  if (comps.originalEffectsReferencePoint) {
959
1073
  desc.origFXRefPoint = { Hrzn: comps.originalEffectsReferencePoint.x, Vrtc: comps.originalEffectsReferencePoint.y };
960
1074
  }
961
1075
 
962
- desc.LyrI = id;
1076
+ desc.LyrI = getLayerId(target, options);
963
1077
  desc.layerSettings = [];
964
1078
 
965
1079
  for (const item of comps.settings) {
@@ -975,12 +1089,22 @@ addHandler(
975
1089
  writeVersionAndDescriptor(writer, '', 'null', desc);
976
1090
  }, true);
977
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
+ }
978
1102
  },
979
1103
  );
980
1104
 
981
1105
  addHandler(
982
1106
  'PxSc',
983
- () => false,
1107
+ hasKey('pixelSource'),
984
1108
  (reader, target) => {
985
1109
  const desc = readVersionAndDescriptor(reader, true) as PxScDescriptor;
986
1110
  // console.log('PxSc', require('util').inspect(desc, false, 99, true));
@@ -1017,7 +1141,7 @@ addHandler(
1017
1141
  _classID: 'PixelSource',
1018
1142
  pixelSourceType: 1986285651, // vdP
1019
1143
  descVersion: 1,
1020
- origin: { Hrzn: source.origin.x, Vrtc: source.origin.y },
1144
+ origin: { Hrzn: source.origin.x, Vrtc: source.origin.y }, // written as 'null' class with 'long' values (see writeDescriptorStructure)
1021
1145
  interpretation: {
1022
1146
  _name: '',
1023
1147
  _classID: 'footageInterpretation',
@@ -1066,7 +1190,7 @@ addHandler(
1066
1190
  lineAlignment: strokeStyleLineAlignment.decode(desc.strokeStyleLineAlignment),
1067
1191
  scaleLock: desc.strokeStyleScaleLock,
1068
1192
  strokeAdjust: desc.strokeStyleStrokeAdjust,
1069
- lineDashSet: desc.strokeStyleLineDashSet.map(parseUnits),
1193
+ lineDashSet: (desc.strokeStyleLineDashSet || []).map(parseUnits),
1070
1194
  blendMode: BlnM.decode(desc.strokeStyleBlendMode),
1071
1195
  opacity: parsePercent(desc.strokeStyleOpacity),
1072
1196
  content: parseVectorContent(desc.strokeStyleContent),
@@ -1105,8 +1229,8 @@ interface ArtbDescriptor {
1105
1229
  artboardRect: { 'Top ': number; Left: number; Btom: number; Rght: number; };
1106
1230
  guideIndeces: any[];
1107
1231
  artboardPresetName: string;
1108
- 'Clr ': DescriptorColor;
1109
- artboardBackgroundType: number;
1232
+ 'Clr '?: DescriptorColor;
1233
+ artboardBackgroundType?: number;
1110
1234
  }
1111
1235
 
1112
1236
  addHandler(
@@ -1119,10 +1243,11 @@ addHandler(
1119
1243
  rect: { top: rect['Top '], left: rect.Left, bottom: rect.Btom, right: rect.Rght },
1120
1244
  guideIndices: desc.guideIndeces,
1121
1245
  presetName: desc.artboardPresetName,
1122
- color: parseColor(desc['Clr ']),
1123
- backgroundType: desc.artboardBackgroundType,
1124
1246
  };
1125
1247
 
1248
+ if (desc['Clr ']) target.artboard.color = parseColor(desc['Clr ']);
1249
+ if (desc.artboardBackgroundType !== undefined) target.artboard.backgroundType = desc.artboardBackgroundType;
1250
+
1126
1251
  skipBytes(reader, left());
1127
1252
  },
1128
1253
  (writer, target) => {
@@ -1269,13 +1394,13 @@ function encodeWarp(warp: Warp): WarpDescriptor {
1269
1394
 
1270
1395
  addHandler(
1271
1396
  'PlLd',
1272
- hasKey('placedLayer'),
1397
+ target => !!target.placedLayer && !target.placedLayer.external, // linked smart objects are stored in SoLE
1273
1398
  (reader, target, left) => {
1274
1399
  if (readSignature(reader) !== 'plcL') throw new Error(`Invalid PlLd signature`);
1275
1400
  if (readInt32(reader) !== 3) throw new Error(`Invalid PlLd version`);
1276
1401
  const id = readPascalString(reader, 1);
1277
1402
  const pageNumber = readInt32(reader);
1278
- const totalPages = readInt32(reader); // TODO: check how this works ?
1403
+ const totalPages = readInt32(reader); // for multi-page documents (PDF)
1279
1404
  readInt32(reader); // anitAliasPolicy 16
1280
1405
  const placedLayerType = readInt32(reader); // 0 = unknown, 1 = vector, 2 = raster, 3 = image stack
1281
1406
  if (!placedLayerTypes[placedLayerType]) throw new Error('Invalid PlLd type');
@@ -1827,7 +1952,7 @@ type SoLdDescriptorFilterItem = {
1827
1952
  'Fl ': string;
1828
1953
  };
1829
1954
  } | {
1830
- 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
1831
1956
  Fltr: {
1832
1957
  _name: 'Rigid Transform';
1833
1958
  _classID: 'rigidTransform';
@@ -1997,7 +2122,7 @@ type SoLdDescriptorFilterItem = {
1997
2122
  Fltr: {
1998
2123
  _name: 'Perspective Warp';
1999
2124
  _classID: 'perspectiveWarpTransform';
2000
- quads: { indices: number[]; }[];
2125
+ quads: { _name?: ''; _classID?: string; indices: number[]; }[];
2001
2126
  vertices: HrznVrtcDescriptor[];
2002
2127
  warpedVertices: HrznVrtcDescriptor[];
2003
2128
  };
@@ -2006,7 +2131,7 @@ type SoLdDescriptorFilterItem = {
2006
2131
  Fltr: {
2007
2132
  _name: 'Curves';
2008
2133
  _classID: 'Crvs';
2009
- presetKind: string; // 'presetKindType.presetKindCustom';
2134
+ presetKind?: string; // 'presetKindType.presetKindCustom';
2010
2135
  Adjs?: {
2011
2136
  _name: '';
2012
2137
  _classID: 'CrvA';
@@ -2015,6 +2140,14 @@ type SoLdDescriptorFilterItem = {
2015
2140
  Mpng?: number[];
2016
2141
  }[];
2017
2142
  };
2143
+ } | {
2144
+ filterID: 1282829427;
2145
+ Fltr: {
2146
+ _name: 'Levels';
2147
+ _classID: 'Lvls';
2148
+ presetKind?: string; // 'presetKindType.presetKindCustom';
2149
+ Adjs?: FilterLevelsAdjustment[];
2150
+ };
2018
2151
  } | {
2019
2152
  filterID: 1231976050;
2020
2153
  } | {
@@ -2036,6 +2169,18 @@ interface FilterCurvesCurvePoint {
2036
2169
  Cnty?: boolean;
2037
2170
  }
2038
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
+
2039
2184
  interface SoLdDescriptorFilter {
2040
2185
  _name: '',
2041
2186
  _classID: 'filterFXStyle',
@@ -2048,11 +2193,13 @@ interface SoLdDescriptorFilter {
2048
2193
  }
2049
2194
 
2050
2195
  function uint8ToFloat32(array: Uint8Array) {
2051
- return new Float32Array(array.buffer.slice(array.byteOffset), 0, array.byteLength / 4);
2196
+ const length = Math.floor(array.byteLength / 4);
2197
+ return new Float32Array(array.buffer.slice(array.byteOffset, array.byteOffset + length * 4), 0, length);
2052
2198
  }
2053
2199
 
2054
2200
  function uint8ToUint32(array: Uint8Array) {
2055
- return new Uint32Array(array.buffer.slice(array.byteOffset), 0, array.byteLength / 4);
2201
+ const length = Math.floor(array.byteLength / 4);
2202
+ return new Uint32Array(array.buffer.slice(array.byteOffset, array.byteOffset + length * 4), 0, length);
2056
2203
  }
2057
2204
 
2058
2205
  function toUint8(array: Uint32Array | Float32Array) {
@@ -2097,7 +2244,7 @@ function pointToHrznVrtc(point: { x: UnitsValue; y: UnitsValue; }): HrznVrtcDesc
2097
2244
  };
2098
2245
  }
2099
2246
 
2100
- function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): Filter | undefined {
2247
+ function parseFilterFXItem(f: SoLdDescriptorFilterItem, reader: PsdReader): Filter | undefined {
2101
2248
  const base: Omit<Filter, 'type' | 'filter'> = {
2102
2249
  name: f['Nm '],
2103
2250
  opacity: parsePercent(f.blendOptions.Opct),
@@ -2177,7 +2324,7 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2177
2324
  undefinedAreas: UndA.decode(f.Fltr.UndA),
2178
2325
  displacementFile: {
2179
2326
  signature: f.Fltr.DspF.sig,
2180
- path: f.Fltr.DspF.path, // TODO: this is decoded incorrectly ???
2327
+ path: f.Fltr.DspF.path,
2181
2328
  },
2182
2329
  },
2183
2330
  };
@@ -2510,10 +2657,9 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2510
2657
  { x: f.Fltr.PuX2, y: f.Fltr.PuY2, },
2511
2658
  { x: f.Fltr.PuX3, y: f.Fltr.PuY3, },
2512
2659
  ],
2513
- puppetShapeList: f.Fltr.puppetShapeList!.map(p => ({
2660
+ puppetShapeList: (f.Fltr.puppetShapeList || []).map(p => ({
2514
2661
  rigidType: p.rigidType,
2515
- // TODO: VrsM
2516
- // TODO: VrsN
2662
+ // VrsM / VrsN - version (always 1.0)
2517
2663
  originalVertexArray: uint8ToPoints(p.originalVertexArray),
2518
2664
  deformedVertexArray: uint8ToPoints(p.deformedVertexArray),
2519
2665
  indexArray: Array.from(uint8ToUint32(p.indexArray)),
@@ -2632,7 +2778,8 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2632
2778
  ...base,
2633
2779
  type: 'curves',
2634
2780
  filter: {
2635
- 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) } : {}),
2636
2783
  ...(f.Fltr.Adjs ? {
2637
2784
  adjustments: f.Fltr.Adjs.map(a => {
2638
2785
  const channels = a.Chnl.map(Chnl.decode);
@@ -2655,6 +2802,40 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2655
2802
  },
2656
2803
  };
2657
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
+ };
2658
2839
  case 'BrgC': {
2659
2840
  return {
2660
2841
  ...base,
@@ -2667,10 +2848,11 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2667
2848
  };
2668
2849
  };
2669
2850
  default:
2670
- if (options.throwForMissingFeatures) {
2851
+ if (reader.throwForMissingFeatures) {
2671
2852
  // console.log('FILTER', require('util').inspect(f, false, 99, true));
2672
2853
  throw new Error(`Unknown filter classId: ${(f as any).Fltr._classID}`);
2673
2854
  }
2855
+ reader.logMissingFeatures && reader.log(`Unhandled smart filter: ${(f as any).Fltr._classID} (${f['Nm ']})`);
2674
2856
  return undefined;
2675
2857
  }
2676
2858
  } else {
@@ -2689,22 +2871,24 @@ function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): F
2689
2871
  case 1314149187: return { ...base, type: 'ntsc colors' };
2690
2872
  case 1231976050: return { ...base, type: 'invert' }
2691
2873
  default:
2692
- if (options.throwForMissingFeatures) {
2874
+ if (reader.throwForMissingFeatures) {
2693
2875
  // console.log('FILTER', require('util').inspect(f, false, 99, true));
2694
2876
  throw new Error(`Unknown filterID: ${(f as any).filterID}`);
2695
2877
  }
2878
+ reader.logMissingFeatures && reader.log(`Unhandled smart filter: ${(f as any).filterID} (${f['Nm ']})`);
2879
+ return undefined;
2696
2880
  }
2697
2881
  }
2698
2882
  }
2699
2883
 
2700
- function parseFilterFX(desc: SoLdDescriptorFilter, options: ReadOptions): PlacedLayerFilter {
2884
+ function parseFilterFX(desc: SoLdDescriptorFilter, reader: PsdReader): PlacedLayerFilter {
2701
2885
  return {
2702
2886
  enabled: desc.enab,
2703
2887
  validAtPosition: desc.validAtPosition,
2704
2888
  maskEnabled: desc.filterMaskEnable,
2705
2889
  maskLinked: desc.filterMaskLinked,
2706
2890
  maskExtendWithWhite: desc.filterMaskExtendWithWhite,
2707
- 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),
2708
2892
  };
2709
2893
  }
2710
2894
 
@@ -3243,14 +3427,14 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3243
3427
  Fltr: {
3244
3428
  _name: 'Rigid Transform',
3245
3429
  _classID: 'rigidTransform',
3246
- 'null': ['Ordn.Trgt'], // TODO: ???
3430
+ 'null': ['Ordn.Trgt'], // target layer reference
3247
3431
  rigidType: f.filter.rigidType,
3248
- puppetShapeList: f.filter.puppetShapeList.map(p => ({
3432
+ puppetShapeList: (f.filter.puppetShapeList || []).map(p => ({
3249
3433
  _name: '',
3250
3434
  _classID: 'puppetShape',
3251
3435
  rigidType: p.rigidType,
3252
- VrsM: 1, // TODO: ???
3253
- VrsN: 0, // TODO: ???
3436
+ VrsM: 1, // version major
3437
+ VrsN: 0, // version minor
3254
3438
  originalVertexArray: toUint8(new Float32Array(pointsToArray(p.originalVertexArray))),
3255
3439
  deformedVertexArray: toUint8(new Float32Array(pointsToArray(p.deformedVertexArray))),
3256
3440
  indexArray: toUint8(new Uint32Array(p.indexArray)),
@@ -3356,7 +3540,7 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3356
3540
  _classID: 'perspectiveWarpTransform',
3357
3541
  vertices: f.filter.vertices.map(pointToHrznVrtc),
3358
3542
  warpedVertices: f.filter.warpedVertices.map(pointToHrznVrtc),
3359
- quads: f.filter.quads.map(indices => ({ indices })),
3543
+ quads: f.filter.quads.map(indices => ({ _name: '', _classID: 'quadIndices', indices })),
3360
3544
  },
3361
3545
  filterID: 442,
3362
3546
  };
@@ -3365,7 +3549,7 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3365
3549
  Fltr: {
3366
3550
  _name: 'Curves',
3367
3551
  _classID: 'Crvs',
3368
- presetKind: presetKindType.encode(f.filter.presetKind),
3552
+ ...(f.filter.presetKind !== undefined ? { presetKind: presetKindType.encode(f.filter.presetKind) } : {}),
3369
3553
  ...(f.filter.adjustments ? {
3370
3554
  Adjs: f.filter.adjustments.map(a => 'curve' in a ? {
3371
3555
  _name: '',
@@ -3388,6 +3572,28 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3388
3572
  },
3389
3573
  filterID: 1131574899,
3390
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
+ };
3391
3597
  case 'brightness/contrast': return {
3392
3598
  ...base,
3393
3599
  Fltr: {
@@ -3399,10 +3605,17 @@ function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem {
3399
3605
  },
3400
3606
  filterID: 1114793795,
3401
3607
  };
3402
- // case 'hsb/hsl': return {
3403
- // TODO: ...
3404
- // };
3405
- default: throw new Error(`Unknow filter type: ${(f as any).type}`);
3608
+ case 'hsb/hsl': return {
3609
+ ...base,
3610
+ Fltr: {
3611
+ _name: 'HSB/HSL',
3612
+ _classID: 'HsbP',
3613
+ Inpt: ClrS.encode(f.filter.inputMode),
3614
+ Otpt: ClrS.encode(f.filter.rowOrder),
3615
+ },
3616
+ filterID: 1215521360,
3617
+ };
3618
+ default: throw new Error(`Unknown filter type: ${(f as any).type}`);
3406
3619
  }
3407
3620
  }
3408
3621
 
@@ -3424,8 +3637,8 @@ interface SoLdDescriptor {
3424
3637
  'Sz ': { _name: '', _classID: 'Pnt ', Wdth: number; Hght: number; };
3425
3638
  Rslt: DescriptorUnitsValue;
3426
3639
  filterFX?: SoLdDescriptorFilter;
3427
- comp?: number; // TODO: support this ?
3428
- compInfo?: { compID: number; originalCompID: number; }; // TODO: support this ?
3640
+ comp?: number;
3641
+ compInfo?: { compID: number; originalCompID: number; };
3429
3642
  Impr?: {}; // ???
3430
3643
  ClMg?: { // TODO: support this ?
3431
3644
  _name: '';
@@ -3473,7 +3686,7 @@ function getWarpFromPlacedLayer(placed: PlacedLayer): Warp {
3473
3686
 
3474
3687
  addHandler(
3475
3688
  'SoLd',
3476
- hasKey('placedLayer'),
3689
+ target => !!target.placedLayer && !target.placedLayer.external, // linked smart objects are stored in SoLE
3477
3690
  (reader, target, left) => {
3478
3691
  if (readSignature(reader) !== 'soLD') throw new Error(`Invalid SoLd type`);
3479
3692
  const version = readInt32(reader);
@@ -3531,6 +3744,15 @@ addHandler(
3531
3744
  throw new Error('Placed layer ID must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4)');
3532
3745
  }
3533
3746
 
3747
+ const placedType = placedLayerTypes.indexOf(placed.type);
3748
+ if (placedType === -1) throw new Error('Invalid placedLayer type');
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
+
3534
3756
  const desc: SoLdDescriptor = {
3535
3757
  Idnt: placed.id,
3536
3758
  placed: placed.placed ?? placed.id,
@@ -3541,16 +3763,16 @@ addHandler(
3541
3763
  duration: placed.duration || { numerator: 0, denominator: 600 },
3542
3764
  frameCount: placed.frameCount || 0,
3543
3765
  Annt: 16,
3544
- Type: placedLayerTypes.indexOf(placed.type),
3766
+ Type: placedType,
3545
3767
  Trnf: placed.transform,
3546
3768
  nonAffineTransform: placed.nonAffineTransform ?? placed.transform,
3547
3769
  // quiltWarp: {} as any,
3548
- warp: encodeWarp(getWarpFromPlacedLayer(placed)),
3770
+ warp: encodeWarp(warp),
3549
3771
  'Sz ': {
3550
3772
  _name: '',
3551
3773
  _classID: 'Pnt ',
3552
- Wdth: placed.width || 0, // TODO: find size ?
3553
- Hght: placed.height || 0, // TODO: find size ?
3774
+ Wdth: width,
3775
+ Hght: height,
3554
3776
  },
3555
3777
  Rslt: placed.resolution ? unitsValue(placed.resolution, 'resolution') : { units: 'Density', value: 72 },
3556
3778
  };
@@ -3568,15 +3790,6 @@ addHandler(
3568
3790
  };
3569
3791
  }
3570
3792
 
3571
- // TODO:
3572
- // desc.comp = -1;
3573
- // desc.compInfo = { _name: '', _classID: 'null', compID: -1, originalCompID: -1 } as any;
3574
- // desc.ClMg = {
3575
- // _name: '',
3576
- // _classID: 'ClMg',
3577
- // placedLayerOCIOConversion: 'placedLayerOCIOConversion.placedLayerOCIOConvertEmbedded'
3578
- // } as any;
3579
-
3580
3793
  // if (JSON.stringify(t) !== JSON.stringify(desc)) {
3581
3794
  // console.log('read', require('util').inspect(t, false, 99, true));
3582
3795
  // console.log('write', require('util').inspect(desc, false, 99, true));
@@ -3608,7 +3821,17 @@ addHandler(
3608
3821
  },
3609
3822
  );
3610
3823
 
3611
- 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
+ );
3612
3835
 
3613
3836
  addHandler(
3614
3837
  'fxrp',
@@ -3625,25 +3848,50 @@ addHandler(
3625
3848
  },
3626
3849
  );
3627
3850
 
3628
- addHandler(
3629
- 'Lr16',
3630
- () => false,
3631
- (reader, _target, _left, psd, imageResources) => {
3632
- readLayerInfo(reader, psd, imageResources);
3633
- },
3634
- (_writer, _target) => {
3635
- },
3636
- );
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
+ }
3637
3869
 
3638
- addHandler(
3639
- 'Lr32',
3640
- () => false,
3641
- (reader, _target, _left, psd, imageResources) => {
3642
- readLayerInfo(reader, psd, imageResources);
3643
- },
3644
- (_writer, _target) => {
3645
- },
3646
- );
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
+ }
3647
3895
 
3648
3896
  addHandler(
3649
3897
  'LMsk',
@@ -3651,7 +3899,7 @@ addHandler(
3651
3899
  (reader, target) => {
3652
3900
  target.userMask = {
3653
3901
  colorSpace: readColor(reader),
3654
- opacity: readUint16(reader) / 0xff,
3902
+ opacity: readUint16(reader) / 100,
3655
3903
  };
3656
3904
  const flag = readUint8(reader);
3657
3905
  if (flag !== 128) throw new Error('Invalid flag value');
@@ -3660,7 +3908,7 @@ addHandler(
3660
3908
  (writer, target) => {
3661
3909
  const userMask = target.userMask!;
3662
3910
  writeColor(writer, userMask.colorSpace);
3663
- writeUint16(writer, clamp(userMask.opacity, 0, 1) * 0xff);
3911
+ writeUint16(writer, Math.round(clamp(userMask.opacity, 0, 1) * 100));
3664
3912
  writeUint8(writer, 128);
3665
3913
  writeZeros(writer, 1);
3666
3914
  },
@@ -3678,11 +3926,12 @@ if (MOCK_HANDLERS) {
3678
3926
  );
3679
3927
  } else {
3680
3928
  addHandler(
3681
- '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
3682
3930
  target => !!(target.patterns && target.patterns.length > 0),
3683
3931
  (reader, target, left) => {
3684
3932
  while (left() > 0) {
3685
- const pattern = readPattern(reader);
3933
+ const pattern = readPatternSafe(reader);
3934
+ if (!pattern) continue;
3686
3935
  if (target.patterns === undefined) target.patterns = [];
3687
3936
  target.patterns.push(pattern);
3688
3937
  }
@@ -3810,7 +4059,8 @@ addHandler(
3810
4059
  const annotations: Annotation[] = [];
3811
4060
 
3812
4061
  for (let i = 0; i < count; i++) {
3813
- /*const length =*/ readUint32(reader);
4062
+ const recordStart = reader.offset;
4063
+ const length = readUint32(reader); // includes the length field itself
3814
4064
  const type = readSignature(reader);
3815
4065
  const open = !!readUint8(reader);
3816
4066
  /*const flags =*/ readUint8(reader); // always 28
@@ -3827,10 +4077,12 @@ addHandler(
3827
4077
  let data: string | Uint8Array;
3828
4078
 
3829
4079
  if (type === 'txtA') {
4080
+ const dataStart = reader.offset;
4081
+
3830
4082
  if (dataLength >= 2 && readUint16(reader) === 0xfeff) {
3831
- data = readUnicodeStringWithLength(reader, (dataLength - 2) / 2);
4083
+ data = readUnicodeStringWithLength(reader, Math.floor((dataLength - 2) / 2));
3832
4084
  } else {
3833
- reader.offset -= 2;
4085
+ reader.offset = dataStart;
3834
4086
  data = readAsciiString(reader, dataLength);
3835
4087
  }
3836
4088
 
@@ -3844,6 +4096,8 @@ addHandler(
3844
4096
  annotations.push({
3845
4097
  type: type === 'txtA' ? 'text' : 'sound', open, iconLocation, popupLocation, color, author, name, date, data,
3846
4098
  });
4099
+
4100
+ if (length >= (reader.offset - recordStart)) reader.offset = recordStart + length;
3847
4101
  }
3848
4102
 
3849
4103
  (target as Psd).annotations = annotations;
@@ -3907,6 +4161,11 @@ interface LinkedFileDescriptor {
3907
4161
  relPath: string;
3908
4162
  }
3909
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
+
3910
4169
  function createLnkHandler(tag: string) {
3911
4170
  addHandler(
3912
4171
  tag,
@@ -3931,8 +4190,8 @@ function createLnkHandler(tag: string) {
3931
4190
  const id = readPascalString(reader, 1);
3932
4191
  const name = readUnicodeString(reader);
3933
4192
 
3934
- const fileType = readSignature(reader).trim(); // ' ' if empty
3935
- 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
3936
4195
  const dataSize = readLength64(reader);
3937
4196
  const hasFileOpenDescriptor = readUint8(reader);
3938
4197
  const fileOpenDescriptor = hasFileOpenDescriptor ? readVersionAndDescriptor(reader) as FileOpenDescriptor : undefined;
@@ -3998,16 +4257,19 @@ function createLnkHandler(tag: string) {
3998
4257
  for (const file of psd.linkedFiles!) {
3999
4258
  if ((tag === 'lnkE') !== !!file.linkedFile) continue;
4000
4259
 
4260
+ const external = tag === 'lnkE';
4001
4261
  let version = 2;
4002
4262
  if (file.assetLockedState != null) version = 7;
4003
4263
  else if (file.assetModTime != null) version = 6;
4004
4264
  else if (file.childDocumentID != null) version = 5;
4005
- else if (tag === 'lnkE') version = 3;
4265
+ // liFE version 2 is the only one with embedded data, it has no date and its file size is the size of the data,
4266
+ // versions 5+ can't store the data, so it's not written in that case
4267
+ else if (external && !file.data) version = 4;
4006
4268
 
4007
- writeLength64(writer, 0);
4269
+ writeUint64(writer, 0);
4008
4270
 
4009
4271
  const sizeOffset = writer.offset;
4010
- writeSignature(writer, (tag === 'lnkE') ? 'liFE' : (file.data ? 'liFD' : 'liFA'));
4272
+ writeSignature(writer, external ? 'liFE' : (file.data ? 'liFD' : 'liFA'));
4011
4273
  writeInt32(writer, version);
4012
4274
  if (!file.id || typeof file.id !== 'string' || !/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(file.id)) {
4013
4275
  throw new Error('Linked file ID must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4)');
@@ -4016,7 +4278,7 @@ function createLnkHandler(tag: string) {
4016
4278
  writeUnicodeStringWithPadding(writer, file.name || '');
4017
4279
  writeSignature(writer, file.type ? `${file.type} `.substring(0, 4) : ' ');
4018
4280
  writeSignature(writer, file.creator ? `${file.creator} `.substring(0, 4) : '\0\0\0\0');
4019
- writeLength64(writer, file.data ? file.data.byteLength : 0);
4281
+ writeUint64(writer, (!external && file.data) ? file.data.byteLength : 0);
4020
4282
 
4021
4283
  if (file.descriptor && file.descriptor.compInfo) {
4022
4284
  const desc: FileOpenDescriptor = {
@@ -4032,7 +4294,7 @@ function createLnkHandler(tag: string) {
4032
4294
  writeUint8(writer, 0);
4033
4295
  }
4034
4296
 
4035
- if (tag === 'lnkE') {
4297
+ if (external) {
4036
4298
  const desc: LinkedFileDescriptor = {
4037
4299
  descVersion: 2,
4038
4300
  'Nm ': file.linkedFile?.name ?? '',
@@ -4042,7 +4304,9 @@ function createLnkHandler(tag: string) {
4042
4304
  };
4043
4305
 
4044
4306
  writeVersionAndDescriptor(writer, '', 'ExternalFileLink', desc);
4307
+ }
4045
4308
 
4309
+ if (external && version > 3) {
4046
4310
  const time = file.time ? new Date(file.time) : new Date();
4047
4311
  writeInt32(writer, time.getUTCFullYear());
4048
4312
  writeUint8(writer, time.getUTCMonth());
@@ -4052,15 +4316,18 @@ function createLnkHandler(tag: string) {
4052
4316
  writeFloat64(writer, time.getUTCSeconds() + time.getUTCMilliseconds() / 1000);
4053
4317
  }
4054
4318
 
4055
- if (file.data) {
4056
- writeBytes(writer, file.data);
4319
+ if (external) {
4320
+ writeUint64(writer, (version === 2 ? file.data?.byteLength : file.linkedFile?.fileSize) || 0); // file size
4321
+ } else if (file.data) {
4322
+ writeBytes(writer, file.data); // liFD
4057
4323
  } else {
4058
- writeLength64(writer, file.linkedFile?.fileSize || 0);
4324
+ writeZeros(writer, 8); // liFA
4059
4325
  }
4060
4326
 
4061
4327
  if (version >= 5) writeUnicodeStringWithPadding(writer, file.childDocumentID || '');
4062
4328
  if (version >= 6) writeFloat64(writer, file.assetModTime || 0);
4063
4329
  if (version >= 7) writeUint8(writer, file.assetLockedState || 0);
4330
+ if (external && version === 2 && file.data) writeBytes(writer, file.data);
4064
4331
 
4065
4332
  let size = writer.offset - sizeOffset;
4066
4333
  writer.view.setUint32(sizeOffset - 4, size, false); // write size
@@ -4082,28 +4349,55 @@ addHandlerAlias('lnk3', 'lnk2');
4082
4349
 
4083
4350
  interface PthsDescriptor {
4084
4351
  pathList: {
4352
+ _name: '';
4085
4353
  _classID: 'pathInfoClass';
4086
4354
  pathUnicodeName: string;
4087
- pathSymmetryClass: {
4355
+ pathSymmetryClass?: {
4356
+ _name: '';
4088
4357
  _classID: 'pathSymmetryClass';
4089
4358
  pathSymmetryMode: string; // 'pathSymmetryModeEnum.pathSymmetryModeBasicPath'
4090
4359
  };
4091
4360
  }[];
4092
4361
  }
4093
4362
 
4363
+ // unicode names of saved paths (path data is stored in image resources 2000-2998)
4094
4364
  addHandler(
4095
4365
  'pths',
4096
- hasKey('pathList'),
4097
- (reader, target) => {
4366
+ target => (target as Psd).imageResources?.savedPaths !== undefined,
4367
+ (reader, _target, left, psd) => {
4098
4368
  const desc = readVersionAndDescriptor(reader, true) as PthsDescriptor;
4099
- // console.log(require('util').inspect(desc, false, 99, true));
4100
- // if (options.throwForMissingFeatures && desc?.pathList?.length) throw new Error('non-empty pathList in `pths`');
4101
- desc;
4102
- 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());
4103
4386
  },
4104
- (writer, _target) => {
4387
+ (writer, _target, psd) => {
4105
4388
  const desc: PthsDescriptor = {
4106
- 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
+ })),
4107
4401
  };
4108
4402
 
4109
4403
  writeVersionAndDescriptor(writer, '', 'pathsDataClass', desc);
@@ -4118,15 +4412,8 @@ addHandler(
4118
4412
  );
4119
4413
 
4120
4414
  addHandler(
4121
- 'lfxs',
4122
- () => false, // TODO: not sure when we actually need to write this section
4123
- // NOTE: this might be insufficient
4124
- // target => target.effects !== undefined && (
4125
- // !!target.effects.dropShadow?.some(e => e.choke) ||
4126
- // !!target.effects.innerShadow?.some(e => e.choke) ||
4127
- // !!target.effects.outerGlow?.choke ||
4128
- // !!target.effects.innerGlow?.choke
4129
- // ),
4415
+ 'lfxs', // effects of group layers (instead of 'lfx2' / 'lmfx')
4416
+ target => target.effects !== undefined && isGroupLayer(target),
4130
4417
  (reader, target, left) => {
4131
4418
  const version = readUint32(reader);
4132
4419
  if (version !== 0) throw new Error(`Invalid lfxs version`);
@@ -4183,6 +4470,12 @@ function readLevelsChannel(reader: PsdReader): LevelsAdjustmentChannel {
4183
4470
  return { shadowInput, highlightInput, shadowOutput, highlightOutput, midtoneInput };
4184
4471
  }
4185
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
+
4186
4479
  function writeLevelsChannel(writer: PsdWriter, channel: LevelsAdjustmentChannel) {
4187
4480
  writeInt16(writer, channel.shadowInput);
4188
4481
  writeInt16(writer, channel.highlightInput);
@@ -4197,62 +4490,49 @@ addHandler(
4197
4490
  (reader, target, left) => {
4198
4491
  if (readUint16(reader) !== 2) throw new Error('Invalid levl version');
4199
4492
 
4200
- const levels: LevelsAdjustmentChannel[] = [];
4493
+ const channels: LevelsAdjustmentChannel[] = [];
4201
4494
 
4202
- for (let i = 0; i < 29 && left(); i++) {
4203
- levels.push(readLevelsChannel(reader));
4495
+ for (let i = 0; i < 29 && left() >= 10; i++) {
4496
+ channels.push(readLevelsChannel(reader));
4204
4497
  }
4205
4498
 
4206
- const [rgb, red, green, blue] = levels;
4207
-
4208
- target.adjustment = {
4209
- ...target.adjustment as PresetInfo,
4210
- type: 'levels',
4211
- rgb,
4212
- red,
4213
- green,
4214
- blue,
4215
- };
4216
-
4217
- if (left()) {
4499
+ if (left() >= 8) {
4218
4500
  if (readSignature(reader) !== 'Lvls') throw new Error(`Invalid levl signature`);
4219
4501
  if (readUint16(reader) !== 3) throw new Error(`Invalid levl version`);
4220
4502
 
4221
4503
  const totalChannelRecords = readUint16(reader);
4222
4504
 
4223
- for (let i = 0; i < (totalChannelRecords - levels.length) && left(); i++) {
4224
- levels.push(readLevelsChannel(reader));
4505
+ while (channels.length < totalChannelRecords && left() >= 10) {
4506
+ channels.push(readLevelsChannel(reader));
4225
4507
  }
4226
4508
  }
4227
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
+
4228
4519
  skipBytes(reader, left());
4229
4520
  },
4230
4521
  (writer, target) => {
4231
- const info = target.adjustment as LevelsAdjustment;
4232
- const defaultChannel = {
4233
- shadowInput: 0,
4234
- highlightInput: 255,
4235
- shadowOutput: 0,
4236
- highlightOutput: 255,
4237
- midtoneInput: 1,
4238
- };
4522
+ const channels = (target.adjustment as LevelsAdjustment).channels || [];
4239
4523
 
4240
4524
  writeUint16(writer, 2); // version
4241
- writeLevelsChannel(writer, info.rgb || defaultChannel);
4242
- writeLevelsChannel(writer, info.red || defaultChannel);
4243
- writeLevelsChannel(writer, info.green || defaultChannel);
4244
- writeLevelsChannel(writer, info.blue || defaultChannel);
4245
4525
 
4246
- for (let i = 0; i < (29 - 4); i++) {
4247
- writeLevelsChannel(writer, defaultChannel);
4526
+ for (let i = 0; i < 29; i++) {
4527
+ writeLevelsChannel(writer, channels[i] || defaultLevelsChannel);
4248
4528
  }
4249
4529
 
4250
4530
  writeSignature(writer, 'Lvls');
4251
4531
  writeUint16(writer, 3); // version
4252
4532
  writeUint16(writer, 62); // total count
4253
4533
 
4254
- for (let i = 0; i < (62 - 29); i++) {
4255
- writeLevelsChannel(writer, defaultChannel);
4534
+ for (let i = 29; i < 62; i++) {
4535
+ writeLevelsChannel(writer, channels[i] || defaultLevelsChannel);
4256
4536
  }
4257
4537
  },
4258
4538
  );
@@ -4286,17 +4566,20 @@ addHandler(
4286
4566
  readUint8(reader);
4287
4567
  if (readUint16(reader) !== 1) throw new Error('Invalid curv version');
4288
4568
  readUint16(reader);
4289
- const channels = readUint16(reader);
4290
- const info: CurvesAdjustment = { type: 'curves' };
4569
+ const channelsMask = readUint16(reader);
4570
+ const channels: CurvesAdjustmentChannel[] = [];
4291
4571
 
4292
- if (channels & 1) info.rgb = readCurveChannel(reader);
4293
- if (channels & 2) info.red = readCurveChannel(reader);
4294
- if (channels & 4) info.green = readCurveChannel(reader);
4295
- 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
+ }
4296
4578
 
4297
4579
  target.adjustment = {
4298
4580
  ...target.adjustment as PresetInfo,
4299
- ...info,
4581
+ type: 'curves',
4582
+ channels,
4300
4583
  };
4301
4584
 
4302
4585
  // ignoring, duplicate information
@@ -4319,35 +4602,33 @@ addHandler(
4319
4602
  skipBytes(reader, left());
4320
4603
  },
4321
4604
  (writer, target) => {
4322
- const info = target.adjustment as CurvesAdjustment;
4323
- const { rgb, red, green, blue } = info;
4324
- let channels = 0;
4325
- let channelCount = 0;
4326
-
4327
- if (rgb && rgb.length) { channels |= 1; channelCount++; }
4328
- if (red && red.length) { channels |= 2; channelCount++; }
4329
- if (green && green.length) { channels |= 4; channelCount++; }
4330
- 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
+ }
4331
4615
 
4332
4616
  writeUint8(writer, 0);
4333
4617
  writeUint16(writer, 1); // version
4334
4618
  writeUint16(writer, 0);
4335
- writeUint16(writer, channels);
4619
+ writeUint16(writer, channelsMask);
4336
4620
 
4337
- if (rgb && rgb.length) writeCurveChannel(writer, rgb);
4338
- if (red && red.length) writeCurveChannel(writer, red);
4339
- if (green && green.length) writeCurveChannel(writer, green);
4340
- if (blue && blue.length) writeCurveChannel(writer, blue);
4621
+ for (const i of used) writeCurveChannel(writer, channels[i]);
4341
4622
 
4342
4623
  writeSignature(writer, 'Crv ');
4343
4624
  writeUint16(writer, 4); // version
4344
4625
  writeUint16(writer, 0);
4345
- writeUint16(writer, channelCount);
4626
+ writeUint16(writer, used.length);
4346
4627
 
4347
- if (rgb && rgb.length) { writeUint16(writer, 0); writeCurveChannel(writer, rgb); }
4348
- if (red && red.length) { writeUint16(writer, 1); writeCurveChannel(writer, red); }
4349
- if (green && green.length) { writeUint16(writer, 2); writeCurveChannel(writer, green); }
4350
- 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
+ }
4351
4632
  },
4352
4633
  );
4353
4634
 
@@ -4432,10 +4713,26 @@ addHandler(
4432
4713
  (reader, target, left) => {
4433
4714
  if (readUint16(reader) !== 2) throw new Error('Invalid hue2 version');
4434
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
+
4435
4722
  target.adjustment = {
4436
4723
  ...target.adjustment as PresetInfo,
4437
4724
  type: 'hue/saturation',
4438
- 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
+ },
4439
4736
  reds: readHueChannel(reader),
4440
4737
  yellows: readHueChannel(reader),
4441
4738
  greens: readHueChannel(reader),
@@ -4450,7 +4747,15 @@ addHandler(
4450
4747
  const info = target.adjustment as HueSaturationAdjustment;
4451
4748
 
4452
4749
  writeUint16(writer, 2); // version
4453
- 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));
4454
4759
  writeHueChannel(writer, info.reds);
4455
4760
  writeHueChannel(writer, info.yellows);
4456
4761
  writeHueChannel(writer, info.greens);
@@ -4564,11 +4869,17 @@ addHandler(
4564
4869
  if (version === 2) {
4565
4870
  color = readColor(reader);
4566
4871
  } else { // version 3
4567
- // TODO: test this, this is probably wrong
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;
4568
4879
  color = {
4569
- l: readInt32(reader) / 100,
4570
- a: readInt32(reader) / 100,
4571
- b: readInt32(reader) / 100,
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))),
4572
4883
  };
4573
4884
  }
4574
4885
 
@@ -4585,7 +4896,7 @@ addHandler(
4585
4896
  const info = target.adjustment as PhotoFilterAdjustment;
4586
4897
  writeUint16(writer, 2); // version
4587
4898
  writeColor(writer, info.color || { l: 0, a: 0, b: 0 });
4588
- writeUint32(writer, (info.density || 0) * 100);
4899
+ writeUint32(writer, Math.round(Math.max(0, info.density || 0) * 100));
4589
4900
  writeUint8(writer, info.preserveLuminosity ? 1 : 0);
4590
4901
  writeZeros(writer, 3);
4591
4902
  },
@@ -4784,7 +5095,7 @@ addHandler(
4784
5095
  const hasMethod = !!readUint8(reader); reader.offset--;
4785
5096
  if (hasMethod) {
4786
5097
  const method = readSignature(reader);
4787
- info.method = gradientInterpolationMethodType.decode(method);
5098
+ info.method = gradientInterpolationMethodType.decode(`gradientInterpolationMethodType.${method}`);
4788
5099
  }
4789
5100
 
4790
5101
  info.name = readUnicodeString(reader);
@@ -4844,8 +5155,9 @@ addHandler(
4844
5155
 
4845
5156
  skipBytes(reader, left());
4846
5157
 
4847
- for (const s of info.colorStops) s.location /= interpolation;
4848
- for (const s of info.opacityStops) s.location /= interpolation;
5158
+ // stop locations are always in 0..4096 range, independent of smoothness
5159
+ for (const s of info.colorStops) s.location /= 4096;
5160
+ for (const s of info.opacityStops) s.location /= 4096;
4849
5161
 
4850
5162
  target.adjustment = info;
4851
5163
  },
@@ -4856,16 +5168,16 @@ addHandler(
4856
5168
  writeUint8(writer, info.dither ? 1 : 0);
4857
5169
 
4858
5170
  if (info.method !== undefined) {
4859
- writeSignature(writer, gradientInterpolationMethodType.encode(info.method));
5171
+ writeSignature(writer, gradientInterpolationMethodType.encode(info.method).split('.')[1]);
4860
5172
  }
4861
5173
 
4862
5174
  writeUnicodeStringWithPadding(writer, info.name || '');
4863
5175
  writeUint16(writer, info.colorStops && info.colorStops.length || 0);
4864
5176
 
4865
- const interpolation = Math.round((info.smoothness ?? 1) * 4096);
5177
+ const interpolation = Math.round(clamp(info.smoothness ?? 1, 0, 1) * 4096);
4866
5178
 
4867
5179
  for (const s of info.colorStops || []) {
4868
- writeUint32(writer, Math.round(s.location * interpolation));
5180
+ writeUint32(writer, Math.round(clamp(s.location, 0, 1) * 4096));
4869
5181
  writeUint32(writer, Math.round(s.midpoint * 100));
4870
5182
  writeColor(writer, s.color);
4871
5183
  writeZeros(writer, 2);
@@ -4874,9 +5186,9 @@ addHandler(
4874
5186
  writeUint16(writer, info.opacityStops && info.opacityStops.length || 0);
4875
5187
 
4876
5188
  for (const s of info.opacityStops || []) {
4877
- writeUint32(writer, Math.round(s.location * interpolation));
5189
+ writeUint32(writer, Math.round(clamp(s.location, 0, 1) * 4096));
4878
5190
  writeUint32(writer, Math.round(s.midpoint * 100));
4879
- writeUint16(writer, Math.round(s.opacity * 0xff));
5191
+ writeUint16(writer, Math.round(clamp(s.opacity, 0, 1) * 0xff));
4880
5192
  }
4881
5193
 
4882
5194
  writeUint16(writer, 2); // expansion count
@@ -5102,7 +5414,7 @@ addHandler(
5102
5414
  target.engineData = fromByteArray(data);
5103
5415
 
5104
5416
  const layersByIndex = getTextLayersSortedByIndex(psd);
5105
- const engineData = parseEngineData(data);
5417
+ const engineData = parseEngineData(data, reader);
5106
5418
  const engineData2 = decodeEngineData2(engineData);
5107
5419
  const TextFrameSet = engineData2.ResourceDict.TextFrameSet;
5108
5420
 
@@ -5230,7 +5542,7 @@ addHandler(
5230
5542
 
5231
5543
  writer.view.setUint32(length2Offset - 4, writer.offset - length2Offset, false);
5232
5544
 
5233
- const extra = target.filterEffectsMasks![target.filterEffectsMasks!.length - 1]?.extra;
5545
+ const extra = mask.extra;
5234
5546
  if (extra) {
5235
5547
  writeUint8(writer, 1);
5236
5548
  writeInt32(writer, extra.top);
@@ -5262,25 +5574,25 @@ addHandler(
5262
5574
  (reader, target) => {
5263
5575
  target.filterMask = {
5264
5576
  colorSpace: readColor(reader),
5265
- opacity: readUint16(reader) / 0xff,
5577
+ opacity: readUint16(reader) / 100,
5266
5578
  };
5267
5579
  },
5268
5580
  (writer, target) => {
5269
5581
  writeColor(writer, target.filterMask!.colorSpace);
5270
- writeUint16(writer, clamp(target.filterMask!.opacity ?? 1, 0, 1) * 0xff);
5582
+ writeUint16(writer, Math.round(clamp(target.filterMask!.opacity ?? 1, 0, 1) * 100));
5271
5583
  },
5272
5584
  );
5273
5585
 
5274
5586
  interface ArtdDescriptor {
5275
5587
  'Cnt ': number;
5276
- autoExpandOffset: { Hrzn: number; Vrtc: number; };
5277
- origin: { Hrzn: number; Vrtc: number; };
5278
- autoExpandEnabled: boolean;
5279
- autoNestEnabled: boolean;
5280
- autoPositionEnabled: boolean;
5588
+ autoExpandOffset?: { Hrzn: number; Vrtc: number; };
5589
+ origin?: { Hrzn: number; Vrtc: number; };
5590
+ autoExpandEnabled?: boolean;
5591
+ autoNestEnabled?: boolean;
5592
+ autoPositionEnabled?: boolean;
5281
5593
  shrinkwrapOnSaveEnabled?: boolean;
5282
- docDefaultNewArtboardBackgroundColor: DescriptorColor;
5283
- docDefaultNewArtboardBackgroundType: number;
5594
+ docDefaultNewArtboardBackgroundColor?: DescriptorColor;
5595
+ docDefaultNewArtboardBackgroundType?: number;
5284
5596
  }
5285
5597
 
5286
5598
  addHandler(
@@ -5288,17 +5600,15 @@ addHandler(
5288
5600
  target => (target as Psd).artboards !== undefined,
5289
5601
  (reader, target, left) => {
5290
5602
  const desc = readVersionAndDescriptor(reader) as ArtdDescriptor;
5291
- (target as Psd).artboards = {
5292
- count: desc['Cnt '],
5293
- autoExpandOffset: { horizontal: desc.autoExpandOffset.Hrzn, vertical: desc.autoExpandOffset.Vrtc },
5294
- origin: { horizontal: desc.origin.Hrzn, vertical: desc.origin.Vrtc },
5295
- autoExpandEnabled: desc.autoExpandEnabled,
5296
- autoNestEnabled: desc.autoNestEnabled,
5297
- autoPositionEnabled: desc.autoPositionEnabled,
5298
- shrinkwrapOnSaveEnabled: !!desc.shrinkwrapOnSaveEnabled,
5299
- docDefaultNewArtboardBackgroundColor: parseColor(desc.docDefaultNewArtboardBackgroundColor),
5300
- docDefaultNewArtboardBackgroundType: desc.docDefaultNewArtboardBackgroundType,
5301
- };
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;
5302
5612
 
5303
5613
  skipBytes(reader, left());
5304
5614
  },
@@ -5319,13 +5629,17 @@ addHandler(
5319
5629
  },
5320
5630
  );
5321
5631
 
5632
+ function isGroupLayer(target: LayerAdditionalInfo) {
5633
+ return !!(target as Layer).children;
5634
+ }
5635
+
5322
5636
  export function hasMultiEffects(effects: LayerEffectsInfo) {
5323
5637
  return Object.keys(effects).map(key => (effects as any)[key]).some(v => Array.isArray(v) && v.length > 1);
5324
5638
  }
5325
5639
 
5326
5640
  addHandler(
5327
5641
  'lfx2',
5328
- target => target.effects !== undefined && !hasMultiEffects(target.effects),
5642
+ target => target.effects !== undefined && !isGroupLayer(target) && !hasMultiEffects(target.effects),
5329
5643
  (reader, target, left) => {
5330
5644
  const version = readUint32(reader);
5331
5645
  if (version !== 0) throw new Error(`Invalid lfx2 version`);
@@ -5333,8 +5647,7 @@ addHandler(
5333
5647
  const desc: Lfx2Descriptor & LmfxDescriptor = readVersionAndDescriptor(reader);
5334
5648
  // console.log('READ', require('util').inspect(desc, false, 99, true));
5335
5649
 
5336
- // TODO: don't discard if we got it from lmfx
5337
- // discard if read in 'lrFX' section
5650
+ // discard if read in 'lrFX' section ('lmfx' and 'lfx2' are never stored together)
5338
5651
  target.effects = parseEffects(desc, !!reader.logMissingFeatures);
5339
5652
 
5340
5653
  skipBytes(reader, left());
@@ -5408,26 +5721,17 @@ addHandler(
5408
5721
  },
5409
5722
  );
5410
5723
 
5411
- interface ExtensionDesc {
5412
- generatorSettings: {
5413
- generator_45_assets: { json: string; };
5414
- layerTime: number;
5415
- };
5416
- }
5417
-
5418
- // extension settings ?, ignore it
5724
+ // generator settings of the document (layers store them in 'shmd' section)
5419
5725
  addHandler(
5420
5726
  'extn',
5421
- target => (target as any)._extn !== undefined,
5422
- (reader, target) => {
5423
- const desc: ExtensionDesc = readVersionAndDescriptor(reader);
5424
- // console.log(require('util').inspect(desc, false, 99, true));
5425
-
5426
- 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());
5427
5732
  },
5428
5733
  (writer, target) => {
5429
- // TODO: need to add correct types for desc fields (resources/src.psd)
5430
- if (MOCK_HANDLERS) writeVersionAndDescriptor(writer, '', 'null', (target as any)._extn);
5734
+ writeVersionAndDescriptor(writer, '', 'null', serializeGeneratorSettings(target.generatorSettings!));
5431
5735
  },
5432
5736
  );
5433
5737
 
@@ -5439,7 +5743,7 @@ addHandler(
5439
5743
  skipBytes(reader, 3);
5440
5744
  },
5441
5745
  (writer, target) => {
5442
- writeUint8(writer, target.fillOpacity! * 0xff);
5746
+ writeUint8(writer, Math.round(clamp(target.fillOpacity!, 0, 1) * 0xff));
5443
5747
  writeZeros(writer, 3);
5444
5748
  },
5445
5749
  );
@@ -5450,7 +5754,7 @@ addHandler(
5450
5754
  (reader, target, left) => {
5451
5755
  target.channelBlendingRestrictions = [];
5452
5756
 
5453
- while (left() > 4) {
5757
+ while (left() >= 4) {
5454
5758
  target.channelBlendingRestrictions.push(readInt32(reader));
5455
5759
  }
5456
5760
  },