ag-psd 32.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  22. package/CHANGELOG.md +78 -0
  23. package/LICENSE +5 -0
  24. package/README.md +3 -6
  25. package/README_PSD.md +242 -81
  26. package/dist/abr.d.ts +11 -4
  27. package/dist/abr.js +357 -230
  28. package/dist/abr.js.map +1 -1
  29. package/dist/additionalInfo.d.ts +54 -2
  30. package/dist/additionalInfo.js +487 -306
  31. package/dist/additionalInfo.js.map +1 -1
  32. package/dist/ase.d.ts +6 -1
  33. package/dist/ase.js +61 -61
  34. package/dist/ase.js.map +1 -1
  35. package/dist/bundle.js +1937 -1115
  36. package/dist/descriptor.d.ts +14 -8
  37. package/dist/descriptor.js +202 -186
  38. package/dist/descriptor.js.map +1 -1
  39. package/dist/helpers.d.ts +1 -1
  40. package/dist/helpers.js +4 -2
  41. package/dist/helpers.js.map +1 -1
  42. package/dist/imageResources.d.ts +15 -3
  43. package/dist/imageResources.js +503 -142
  44. package/dist/imageResources.js.map +1 -1
  45. package/dist/jpeg.js +8 -8
  46. package/dist/jpeg.js.map +1 -1
  47. package/dist/psd.d.ts +148 -17
  48. package/dist/psd.js.map +1 -1
  49. package/dist/psdReader.d.ts +10 -0
  50. package/dist/psdReader.js +278 -166
  51. package/dist/psdReader.js.map +1 -1
  52. package/dist/psdWriter.d.ts +1 -0
  53. package/dist/psdWriter.js +77 -62
  54. package/dist/psdWriter.js.map +1 -1
  55. package/dist/text.js +17 -6
  56. package/dist/text.js.map +1 -1
  57. package/dist/utf8.js +0 -3
  58. package/dist/utf8.js.map +1 -1
  59. package/dist-es/abr.d.ts +11 -4
  60. package/dist-es/abr.js +357 -230
  61. package/dist-es/abr.js.map +1 -1
  62. package/dist-es/additionalInfo.d.ts +54 -2
  63. package/dist-es/additionalInfo.js +487 -306
  64. package/dist-es/additionalInfo.js.map +1 -1
  65. package/dist-es/ase.d.ts +6 -1
  66. package/dist-es/ase.js +61 -61
  67. package/dist-es/ase.js.map +1 -1
  68. package/dist-es/descriptor.d.ts +14 -8
  69. package/dist-es/descriptor.js +202 -186
  70. package/dist-es/descriptor.js.map +1 -1
  71. package/dist-es/helpers.d.ts +1 -1
  72. package/dist-es/helpers.js +4 -2
  73. package/dist-es/helpers.js.map +1 -1
  74. package/dist-es/imageResources.d.ts +15 -3
  75. package/dist-es/imageResources.js +503 -142
  76. package/dist-es/imageResources.js.map +1 -1
  77. package/dist-es/jpeg.js +8 -8
  78. package/dist-es/jpeg.js.map +1 -1
  79. package/dist-es/psd.d.ts +148 -17
  80. package/dist-es/psd.js.map +1 -1
  81. package/dist-es/psdReader.d.ts +10 -0
  82. package/dist-es/psdReader.js +278 -166
  83. package/dist-es/psdReader.js.map +1 -1
  84. package/dist-es/psdWriter.d.ts +1 -0
  85. package/dist-es/psdWriter.js +77 -62
  86. package/dist-es/psdWriter.js.map +1 -1
  87. package/dist-es/text.js +17 -6
  88. package/dist-es/text.js.map +1 -1
  89. package/dist-es/utf8.js +0 -3
  90. package/dist-es/utf8.js.map +1 -1
  91. package/package.json +2 -2
  92. package/src/abr.ts +375 -235
  93. package/src/additionalInfo.ts +663 -420
  94. package/src/ase.ts +66 -61
  95. package/src/descriptor.ts +199 -195
  96. package/src/helpers.ts +4 -1
  97. package/src/imageResources.ts +632 -168
  98. package/src/jpeg.ts +8 -8
  99. package/src/psd.ts +156 -30
  100. package/src/psdReader.ts +273 -172
  101. package/src/psdWriter.ts +95 -58
  102. package/src/text.ts +17 -6
  103. package/src/utf8.ts +0 -4
package/src/descriptor.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createEnum, normalizeLabAB, denormalizeLabAB } from './helpers';
2
2
  import {
3
- AntiAlias, BevelDirection, BevelStyle, BevelTechnique, BlendMode, Color, EffectContour,
3
+ AntiAlias, BevelDirection, BevelStyle, BevelTechnique, BlendMode, Color, ColorStop, EffectContour,
4
4
  EffectNoiseGradient, EffectPattern, EffectSolidGradient, ExtraGradientInfo, ExtraPatternInfo,
5
5
  GlowSource, GlowTechnique, GradientStyle, InterpolationMethod, LayerEffectBevel,
6
6
  LayerEffectGradientOverlay, LayerEffectInnerGlow, LayerEffectPatternOverlay,
@@ -149,6 +149,7 @@ const fieldToArrayExtType: ExtTypeDict = {
149
149
  Clrs: makeType('', 'Clrt'),
150
150
  Trns: makeType('', 'TrnS'),
151
151
  keyDescriptorList: nullType,
152
+ keyRootDescriptorList: nullType,
152
153
  solidFillMulti: makeType('', 'SoFi'),
153
154
  gradientFillMulti: makeType('', 'GrFl'),
154
155
  dropShadowMulti: makeType('', 'DrSh'),
@@ -183,17 +184,23 @@ const fieldToArrayExtType: ExtTypeDict = {
183
184
 
184
185
  const typeToField: { [key: string]: string[]; } = {
185
186
  'TEXT': [
186
- 'Txt ', 'printerName', 'Nm ', 'Idnt', 'blackAndWhitePresetFileName', 'LUT3DFileName',
187
+ 'Txt ', 'printerName', 'Nm ', 'Idnt', 'blackAndWhitePresetFileName', 'LUT3DFileName', 'exportOption', 'scaleSettings',
188
+ 'BackgroundImagePath', 'ImageSubfolderName',
187
189
  'presetFileName', 'curvesPresetFileName', 'mixerPresetFileName', 'placed', 'description', 'reason',
188
190
  'artboardPresetName', 'json', 'clipID', 'relPath', 'fullPath', 'mediaDescriptor', 'Msge',
189
- 'altTag', 'url', 'cellText', 'preset', 'KnNm', 'FPth', 'comment', 'originalPath',
191
+ 'altTag', 'url', 'cellText', 'preset', 'KnNm', 'FPth', 'comment', 'originalPath', 'Bk ', 'pathUnicodeName',
190
192
  ],
191
193
  'tdta': [
192
194
  'EngineData', 'LUT3DFileData', 'indexArray', 'originalVertexArray', 'deformedVertexArray',
193
- 'LqMe',
195
+ 'LqMe', 'bookKey',
194
196
  ],
195
197
  'long': [
196
- 'TextIndex', 'RndS', 'Mdpn', 'Smth', 'Lctn', 'strokeStyleVersion', 'LaID', 'Vrsn', 'Cnt ',
198
+ 'TextIndex', 'RndS', 'Mdpn', 'Smth', 'Lctn', 'strokeStyleVersion', 'LaID', 'Vrsn', 'Cnt ', 'bookID',
199
+ 'cropRectBottom', 'cropRectLeft', 'cropRectRight', 'cropRectTop',
200
+ // save for web output settings
201
+ 'Version', 'BackgroundColorBlue', 'BackgroundColorGreen', 'BackgroundColorRed', 'BackgroundColorState', 'AttributeCase', 'Encoding',
202
+ 'DuplicateFileNameBehavior', 'ImageMapLocation', 'ImageMapType', 'Indent', 'LineEndings', 'SpacersEmptyCells',
203
+ 'SpacersHorizontal', 'SpacersVertical', 'StylesFormat', 'TDWidthHeight', 'TagCase', 'ColorPolicy', 'MetadataPolicy',
197
204
  'Brgh', 'Cntr', 'means', 'vibrance', 'Strt', 'bwPresetKind', 'comp', 'compID', 'originalCompID',
198
205
  'curvesPresetKind', 'mixerPresetKind', 'uOrder', 'vOrder', 'PgNm', 'totalPages', 'Crop',
199
206
  'numerator', 'denominator', 'frameCount', 'Annt', 'keyOriginType', 'unitValueQuadVersion',
@@ -215,7 +222,7 @@ const typeToField: { [key: string]: string[]; } = {
215
222
  'tableOrder', 'enableCompCore', 'enableCompCoreGPU', 'compCoreSupport', 'compCoreGPUSupport', 'Engn',
216
223
  'enableCompCoreThreads', 'gs99', 'FrDs', 'trackID', 'animInterpStyle', 'horzAlign',
217
224
  'vertAlign', 'bgColorType', 'shapeOperation', 'UndA', 'Wvtp', 'Drct', 'WndM', 'Edg ', 'FlCl', 'IntE',
218
- 'IntC', 'Cnvr', 'Fl ', 'Dstr', 'MztT', 'Lns ', 'ExtT', 'DspM', 'ExtR', 'ZZTy', 'SphM', 'SmBQ', 'placedLayerOCIOConversion', 'gradientsInterpolationMethod',
225
+ 'IntC', 'Cnvr', 'Fl ', 'Dstr', 'MztT', 'Lns ', 'ExtT', 'DspM', 'ExtR', 'ZZTy', 'SphM', 'SmBQ', 'placedLayerOCIOConversion', 'gradientsInterpolationMethod', 'pathSymmetryMode',
219
226
  'interpretAlpha',
220
227
  ],
221
228
  'bool': [
@@ -232,7 +239,7 @@ const typeToField: { [key: string]: string[]; } = {
232
239
  'moreAccurate', 'GpuY', 'LIWy', 'Cnty', 'Lnkd', 'showAlteredVideo', 'lightingOn',
233
240
  ],
234
241
  'doub': [
235
- 'warpValue', 'warpPerspective', 'warpPerspectiveOther', 'Intr', 'Wdth', 'Hght',
242
+ 'warpValue', 'warpPerspective', 'warpPerspectiveOther', 'Intr', 'Wdth', 'Hght', 'Gmm ',
236
243
  'strokeStyleMiterLimit', 'strokeStyleResolution', 'layerTime', 'keyOriginResolution',
237
244
  'xx', 'xy', 'yx', 'yy', 'tx', 'ty', 'FrGA', 'frameRate', 'audioLevel', 'rotation',
238
245
  'X ', 'Y ', 'redFloat', 'greenFloat', 'blueFloat', 'imageResolution',
@@ -246,7 +253,7 @@ const typeToField: { [key: string]: string[]; } = {
246
253
  'topRight', 'topLeft', 'bottomLeft', 'bottomRight', 'ClNs', 'Shrp',
247
254
  ],
248
255
  'VlLs': [
249
- 'Crv ', 'Clrs', 'Mnm ', 'Mxm ', 'Trns', 'pathList', 'strokeStyleLineDashSet', 'FrLs', 'slices',
256
+ 'Crv ', 'Clrs', 'Mnm ', 'Mxm ', 'Trns', 'pathList', 'keyRootDescriptorList', 'strokeStyleLineDashSet', 'FrLs', 'slices',
250
257
  'LaSt', 'Trnf', 'nonAffineTransform', 'keyDescriptorList', 'guideIndeces', 'gradientFillMulti',
251
258
  'solidFillMulti', 'frameFXMulti', 'innerShadowMulti', 'dropShadowMulti', 'FrIn', 'FSts', 'FsFr',
252
259
  'sheetTimelineOptions', 'audioClipList', 'trackList', 'globalTrackList', 'keyList', 'audioClipList',
@@ -268,53 +275,37 @@ const channels = [
268
275
  // color classes whose channel values are written as doubles ('GRYC' and 'LABC' are kept for backwards compatibility)
269
276
  const doubleColorClasses = ['Grsc', 'LbCl', 'GRYC', 'LABC'];
270
277
 
271
- // Hidden (non-enumerable) markers used to remember how values were stored in the file, so they can be
272
- // written back the same way. They don't change the shape of the objects returned by the reader.
273
- const osTypeKey = typeof Symbol !== 'undefined' ? Symbol('agPsdOSType') : '__agPsdOSType';
274
- const obArInfoKey = typeof Symbol !== 'undefined' ? Symbol('agPsdObAr') : '__agPsdObAr';
275
- const referenceInfoKey = typeof Symbol !== 'undefined' ? Symbol('agPsdReference') : '__agPsdReference';
276
-
277
- function setHidden<T extends object>(target: T, key: symbol | string, value: any): T {
278
- Object.defineProperty(target, key, { value, enumerable: false, configurable: true, writable: true });
279
- return target;
280
- }
281
-
282
- function getHidden(target: any, key: symbol | string): any {
283
- return (target && typeof target === 'object') ? target[key] : undefined;
284
- }
285
-
286
- const osTypeFamilies: { [key: string]: string; } = {
287
- UntF: 'UntF', UnFl: 'UntF',
288
- Objc: 'Objc', GlbO: 'Objc',
289
- type: 'type', GlbC: 'type',
290
- comp: 'comp',
291
- };
292
-
293
- // returns the OS type the value was originally read with, if it's compatible with the type we'd write it as
294
278
  // fallback for unknown keys, based on the type of the value
295
279
  function guessOSType(value: any): string | undefined {
296
280
  if (typeof value === 'boolean') return 'bool';
297
281
  if (typeof value === 'number' && !Number.isInteger(value)) return 'doub';
298
282
  if (Array.isArray(value)) return 'VlLs';
299
- if (value && typeof value === 'object' && typeof value._classID === 'string') return 'Objc';
300
- return undefined;
301
- }
302
283
 
303
- function getRecordedOSType(value: any, type: string | undefined): string | undefined {
304
- const recorded: string | undefined = getHidden(value, osTypeKey);
305
- if (!recorded) return type;
306
- if (!type || recorded === 'comp' || recorded === 'type' || recorded === 'GlbC') return recorded;
307
- return osTypeFamilies[recorded] === osTypeFamilies[type] ? recorded : type;
284
+ if (value && typeof value === 'object') {
285
+ if (typeof value._classID === 'string') return 'Objc';
286
+ if (typeof value.classID === 'string') return 'type'; // class read from 'type' or 'GlbC'
287
+ if (typeof value.low === 'number' && typeof value.high === 'number') return 'comp';
288
+ if (typeof value.units === 'string' && typeof value.value === 'number') return 'UntF';
289
+ }
290
+
291
+ return undefined;
308
292
  }
309
293
 
310
294
  const fieldToArrayType: Dict = {
311
295
  'Mnm ': 'long',
312
296
  'Mxm ': 'long',
297
+ HtmlFileNameComponents: 'long', // save for web output settings
298
+ SavingFileNameComponents: 'long',
299
+ SliceFileNameComponents: 'long',
300
+ Inpt: 'long', // levels filter
301
+ Otpt: 'long', // levels filter
313
302
  FrLs: 'long',
314
303
  strokeStyleLineDashSet: 'UntF',
315
304
  Trnf: 'doub',
316
305
  nonAffineTransform: 'doub',
317
306
  keyDescriptorList: 'Objc',
307
+ keyRootDescriptorList: 'Objc',
308
+ pathList: 'Objc',
318
309
  gradientFillMulti: 'Objc',
319
310
  solidFillMulti: 'Objc',
320
311
  frameFXMulti: 'Objc',
@@ -396,6 +387,12 @@ function getTypeByKey(key: string, value: any, root: string, parent: any) {
396
387
  return 'long';
397
388
  } else if ((key === 'Inpt' || key === 'Otpt') && parent._classID === 'HsbP') {
398
389
  return 'enum';
390
+ } else if ((key === 'Inpt' || key === 'Otpt') && parent._classID === 'LvlA') {
391
+ return 'VlLs';
392
+ } else if (key === 'Intr' && typeof value === 'boolean') {
393
+ return 'bool'; // interpolation in print style
394
+ } else if (key === 'PgPs') {
395
+ return 'enum';
399
396
  } else if (key === 'LghD' && parent._classID === 'oilPaint') {
400
397
  return 'long';
401
398
  } else {
@@ -409,7 +406,7 @@ export function readAsciiStringOrClassId(reader: PsdReader) {
409
406
  }
410
407
 
411
408
  export function writeAsciiStringOrClassId(writer: PsdWriter, value: string) {
412
- if (value.length === 4 && value !== 'warp' && value !== 'time' && value !== 'hold' && value !== 'list') {
409
+ if (value.length === 4 && value !== 'warp' && value !== 'time' && value !== 'hold' && value !== 'list' && value !== 'json') {
413
410
  // write classId
414
411
  writeInt32(writer, 0);
415
412
  writeSignature(writer, value);
@@ -441,12 +438,21 @@ export function readDescriptorStructure(reader: PsdReader, includeClass: boolean
441
438
  return object;
442
439
  }
443
440
 
441
+ export const outputSettingsRoot = 'outputSettings';
442
+
444
443
  export function writeDescriptorStructure(writer: PsdWriter, name: string, classId: string, value: any, root: string) {
445
444
  if (logErrors && !classId) console.log('Missing classId for: ', name, classId, value);
446
445
 
447
446
  // write class structure
448
447
  writeUnicodeStringWithPadding(writer, name);
449
- writeAsciiStringOrClassId(writer, classId);
448
+
449
+ if (root === outputSettingsRoot && classId === 'null') {
450
+ // Photoshop writes 'null' class id as 4 character string in save for web output settings
451
+ writeInt32(writer, 4);
452
+ writeSignature(writer, classId);
453
+ } else {
454
+ writeAsciiStringOrClassId(writer, classId);
455
+ }
450
456
 
451
457
  const allKeys = Object.keys(value);
452
458
  const keys = allKeys.filter(key => key !== '_name' && key !== '_classID' && value[key] !== undefined);
@@ -533,12 +539,14 @@ export function writeDescriptorStructure(writer: PsdWriter, name: string, classI
533
539
  extType = makeType('', 'classFloatRect');
534
540
  }
535
541
 
542
+ // guess color class from its fields (colors created by serializeColor always have _classID set)
536
543
  if (extType && extType.classID === 'RGBC') {
537
544
  if ('H ' in value[key]) extType = { classID: 'HSBC', name: '' };
538
- // TODO: other color spaces
545
+ else if ('Cyn ' in value[key]) extType = { classID: 'CMYC', name: '' };
546
+ else if ('Lmnc' in value[key]) extType = { classID: 'LbCl', name: '' };
547
+ else if ('Gry ' in value[key]) extType = { classID: 'Grsc', name: '' };
539
548
  }
540
549
 
541
- type = getRecordedOSType(value[key], type);
542
550
  if (!type) type = guessOSType(value[key]);
543
551
 
544
552
  writeAsciiStringOrClassId(writer, key);
@@ -553,9 +561,8 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
553
561
  case 'obj ': // Reference
554
562
  return readReferenceStructure(reader);
555
563
  case 'Objc': // Descriptor
556
- return readDescriptorStructure(reader, includeClass);
557
564
  case 'GlbO': // GlobalObject same as Descriptor
558
- return setHidden(readDescriptorStructure(reader, includeClass), osTypeKey, type);
565
+ return readDescriptorStructure(reader, includeClass);
559
566
  case 'VlLs': { // List
560
567
  const length = readInt32(reader);
561
568
  const items: any[] = [];
@@ -580,7 +587,7 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
580
587
  const units = readSignature(reader);
581
588
  const value = readFloat32(reader);
582
589
  if (!unitsMap[units]) throw new Error(`Invalid units: ${units}`);
583
- return setHidden({ units: unitsMap[units], value }, osTypeKey, type);
590
+ return { units: unitsMap[units], value };
584
591
  }
585
592
  case 'TEXT': // String
586
593
  return readUnicodeString(reader);
@@ -594,13 +601,13 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
594
601
  case 'comp': { // Large Integer
595
602
  const low = readUint32(reader);
596
603
  const high = readUint32(reader);
597
- return setHidden({ low, high }, osTypeKey, type);
604
+ return { low, high };
598
605
  }
599
606
  case 'bool': // Boolean
600
607
  return !!readUint8(reader);
601
608
  case 'type': // Class
602
609
  case 'GlbC': // Class
603
- return setHidden(readClassStructure(reader), osTypeKey, type);
610
+ return readClassStructure(reader);
604
611
  case 'alis': { // Alias
605
612
  const length = readInt32(reader);
606
613
  return readAsciiString(reader, length);
@@ -612,24 +619,24 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
612
619
  case 'ObAr': { // Object array
613
620
  readInt32(reader); // version: 16
614
621
  readUnicodeString(reader); // name: ''
615
- const classID = readAsciiStringOrClassId(reader); // 'rationalPoint'
622
+ readAsciiStringOrClassId(reader); // classID: 'rationalPoint'
616
623
  const length = readInt32(reader);
617
624
  const items: any[] = [];
618
625
 
619
626
  for (let i = 0; i < length; i++) {
620
627
  const type1 = readAsciiStringOrClassId(reader); // type Hrzn | Vrtc
621
- const valueType = readSignature(reader); // UnFl
622
- const units = readSignature(reader); // units ? '#Pxl'
628
+ readSignature(reader); // UnFl
629
+ readSignature(reader); // units ? '#Pxl'
623
630
  const valuesCount = readInt32(reader);
624
631
  const values: number[] = [];
625
632
  for (let j = 0; j < valuesCount; j++) {
626
633
  values.push(readFloat64(reader));
627
634
  }
628
635
 
629
- items.push(setHidden({ type: type1, values }, obArInfoKey, { valueType, units }));
636
+ items.push({ type: type1, values });
630
637
  }
631
638
 
632
- return setHidden(items, obArInfoKey, { classID });
639
+ return items;
633
640
  }
634
641
  case 'Pth ': { // File path
635
642
  /*const length =*/ readInt32(reader); // total size of all fields below
@@ -669,7 +676,7 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
669
676
  writeInt32(writer, value.length);
670
677
 
671
678
  for (let i = 0; i < value.length; i++) {
672
- const type = getRecordedOSType(value[i], fieldToArrayType[key]) ?? guessOSType(value[i]);
679
+ const type = fieldToArrayType[key] ?? guessOSType(value[i]);
673
680
  writeSignature(writer, type || 'long');
674
681
  writeOSType(writer, type || 'long', value[i], `${key}[]`, fieldToArrayExtType[key], root);
675
682
  if (logErrors && !type) console.log(`Missing descriptor array type for: '${key}' in`, value);
@@ -733,16 +740,15 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
733
740
  case 'ObAr': { // Object array
734
741
  writeInt32(writer, 16); // version
735
742
  writeUnicodeStringWithPadding(writer, ''); // name
736
- const type = getHidden(value, obArInfoKey)?.classID ?? ObArTypes[key];
743
+ const type = ObArTypes[key];
737
744
  if (!type) throw new Error(`Not implemented ObArType for: ${key}`);
738
745
  writeAsciiStringOrClassId(writer, type);
739
746
  writeInt32(writer, value.length);
740
747
 
741
748
  for (let i = 0; i < value.length; i++) {
742
- const info = getHidden(value[i], obArInfoKey);
743
749
  writeAsciiStringOrClassId(writer, value[i].type); // Hrzn | Vrtc
744
- writeSignature(writer, info?.valueType ?? 'UnFl');
745
- writeSignature(writer, info?.units ?? '#Pxl');
750
+ writeSignature(writer, 'UnFl');
751
+ writeSignature(writer, '#Pxl');
746
752
  writeInt32(writer, value[i].values.length);
747
753
 
748
754
  for (let j = 0; j < value[i].values.length; j++) {
@@ -752,12 +758,13 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
752
758
  break;
753
759
  }
754
760
  case 'Pth ': { // File path
755
- const length = 4 + 4 + 4 + value.path.length * 2;
761
+ const path = value.path + '\0'; // Photoshop stores trailing null character (stripped when reading)
762
+ const length = 4 + 4 + 4 + path.length * 2;
756
763
  writeInt32(writer, length);
757
764
  writeSignature(writer, value.sig);
758
765
  writeInt32LE(writer, length);
759
- writeInt32LE(writer, value.path.length);
760
- writeUnicodeStringWithoutLengthLE(writer, value.path);
766
+ writeInt32LE(writer, path.length);
767
+ writeUnicodeStringWithoutLengthLE(writer, path);
761
768
  break;
762
769
  }
763
770
  default:
@@ -765,86 +772,64 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
765
772
  }
766
773
  }
767
774
 
775
+ // Reference item with explicit reference type and class, can be used in place of a value in reference arrays
776
+ // passed to writeVersionAndDescriptor (references are read as values only: strings, numbers and class objects)
768
777
  export interface DescriptorReferenceItem {
769
778
  type: 'prop' | 'Clss' | 'Enmr' | 'rele' | 'Idnt' | 'indx' | 'name';
770
- name?: string; // class name
771
- classID?: string;
779
+ _name?: string; // class name
780
+ _classID?: string;
772
781
  keyID?: string; // prop
773
782
  typeID?: string; // Enmr
774
783
  enumValue?: string; // Enmr
775
784
  value?: string | number; // rele, Idnt, indx, name
776
785
  }
777
786
 
778
- function referenceItemToValue(item: DescriptorReferenceItem): any {
779
- switch (item.type) {
780
- case 'prop': return item.keyID;
781
- case 'Clss': return { name: item.name || '', classID: item.classID };
782
- case 'Enmr': return `${item.typeID}.${item.enumValue}`;
783
- default: return item.value;
784
- }
785
- }
786
-
787
- function referenceItemMatches(item: DescriptorReferenceItem | undefined, value: any) {
788
- if (!item) return false;
789
- const expected = referenceItemToValue(item);
790
- if (expected && typeof expected === 'object') {
791
- return !!value && typeof value === 'object' && value.name === expected.name && value.classID === expected.classID;
792
- }
793
- return expected === value;
794
- }
787
+ const referenceTypes = ['prop', 'Clss', 'Enmr', 'rele', 'Idnt', 'indx', 'name'];
795
788
 
796
- // Attaches information about how each item of reference should be written (class IDs, reference types),
797
- // the public value of the array stays the same (array of strings/numbers/class objects)
798
- export function createDescriptorReference(items: DescriptorReferenceItem[]): any[] {
799
- return setHidden(items.map(referenceItemToValue), referenceInfoKey, items);
789
+ function isReferenceItem(value: any): value is DescriptorReferenceItem {
790
+ return !!value && typeof value === 'object' && referenceTypes.indexOf(value.type) !== -1;
800
791
  }
801
792
 
802
793
  function readReferenceStructure(reader: PsdReader) {
803
794
  const itemsCount = readInt32(reader);
804
- const infos: DescriptorReferenceItem[] = [];
795
+ const items: any[] = [];
805
796
 
806
797
  for (let i = 0; i < itemsCount; i++) {
807
798
  const type = readSignature(reader);
808
799
 
809
800
  switch (type) {
810
- case 'prop': { // Property
811
- const { name, classID } = readClassStructure(reader);
812
- const keyID = readAsciiStringOrClassId(reader);
813
- infos.push({ type, name, classID, keyID });
801
+ case 'prop': // Property
802
+ readClassStructure(reader);
803
+ items.push(readAsciiStringOrClassId(reader));
814
804
  break;
815
- }
816
- case 'Clss': { // Class
817
- const { name, classID } = readClassStructure(reader);
818
- infos.push({ type, name, classID });
805
+ case 'Clss': // Class
806
+ items.push(readClassStructure(reader));
819
807
  break;
820
- }
821
808
  case 'Enmr': { // Enumerated Reference
822
- const { name, classID } = readClassStructure(reader);
809
+ readClassStructure(reader);
823
810
  const typeID = readAsciiStringOrClassId(reader);
824
811
  const enumValue = readAsciiStringOrClassId(reader);
825
- infos.push({ type, name, classID, typeID, enumValue });
812
+ items.push(`${typeID}.${enumValue}`);
826
813
  break;
827
814
  }
828
- case 'rele': { // Offset
829
- const { name, classID } = readClassStructure(reader);
830
- infos.push({ type, name, classID, value: readUint32(reader) });
815
+ case 'rele': // Offset
816
+ readClassStructure(reader);
817
+ items.push(readUint32(reader));
831
818
  break;
832
- }
833
819
  case 'Idnt': // Identifier
834
820
  case 'indx': // Index
835
- infos.push({ type, value: readInt32(reader) });
821
+ items.push(readInt32(reader));
836
822
  break;
837
- case 'name': { // Name
838
- const { name, classID } = readClassStructure(reader);
839
- infos.push({ type, name, classID, value: readUnicodeString(reader) });
823
+ case 'name': // Name
824
+ readClassStructure(reader);
825
+ items.push(readUnicodeString(reader));
840
826
  break;
841
- }
842
827
  default:
843
828
  throw new Error(`Invalid descriptor reference type: ${type}`);
844
829
  }
845
830
  }
846
831
 
847
- return createDescriptorReference(infos);
832
+ return items;
848
833
  }
849
834
 
850
835
  function guessReferenceItem(value: any): DescriptorReferenceItem {
@@ -854,14 +839,14 @@ function guessReferenceItem(value: any): DescriptorReferenceItem {
854
839
  const match = /^([a-z ]{4}|[a-z]{5,})\.([a-z ]+)$/i.exec(value);
855
840
 
856
841
  if (match) {
857
- return { type: 'Enmr', name: '', classID: match[1], typeID: match[1], enumValue: match[2] };
842
+ return { type: 'Enmr', _name: '', _classID: match[1], typeID: match[1], enumValue: match[2] };
858
843
  } else {
859
- return { type: 'name', name: '', classID: 'Lyr ', value };
844
+ return { type: 'name', _name: '', _classID: 'Lyr ', value };
860
845
  }
861
846
  } else if (typeof value === 'number') {
862
847
  return { type: 'Idnt', value };
863
848
  } else if (value && typeof value === 'object' && typeof value.classID === 'string') {
864
- return { type: 'Clss', name: value.name || '', classID: value.classID };
849
+ return { type: 'Clss', _name: value.name || '', _classID: value.classID };
865
850
  } else {
866
851
  throw new Error(`Invalid descriptor reference value: ${JSON.stringify(value)}`);
867
852
  }
@@ -869,46 +854,39 @@ function guessReferenceItem(value: any): DescriptorReferenceItem {
869
854
 
870
855
  // path selection state (image resource 1088) is a reference to paths, enumerated references there use 'Path' class
871
856
  // (name references use 'Lyr ' class, they reference layer of the vector mask)
872
- export function pathSelectionStateToReference(items: string[]): any[] {
873
- const infos: DescriptorReferenceItem[] | undefined = getHidden(items, referenceInfoKey);
874
-
875
- return createDescriptorReference(items.map((value, i) => {
876
- if (referenceItemMatches(infos?.[i], value)) return infos![i]; // keep information read from the file
877
-
857
+ export function pathSelectionStateToReference(items: string[]): DescriptorReferenceItem[] {
858
+ return items.map(value => {
878
859
  const item = guessReferenceItem(value);
879
- if (item.type === 'Enmr') item.classID = 'Path';
860
+ if (item.type === 'Enmr') item._classID = 'Path';
880
861
  return item;
881
- }));
862
+ });
882
863
  }
883
864
 
884
865
  function writeReferenceStructure(writer: PsdWriter, _key: string, items: any[]) {
885
866
  if (!Array.isArray(items)) throw new Error(`Invalid reference value: ${JSON.stringify(items)}, key: ${_key}`);
886
867
 
887
- const infos: DescriptorReferenceItem[] | undefined = getHidden(items, referenceInfoKey);
888
868
  writeInt32(writer, items.length);
889
869
 
890
- for (let i = 0; i < items.length; i++) {
891
- const value = items[i];
892
- const recorded = infos?.[i];
893
- const item = referenceItemMatches(recorded, value) ? recorded! : guessReferenceItem(value);
870
+ for (const value of items) {
871
+ const item = isReferenceItem(value) ? value : guessReferenceItem(value);
894
872
 
895
873
  writeSignature(writer, item.type);
896
874
 
897
875
  switch (item.type) {
898
876
  case 'prop': // Property
899
- writeClassStructure(writer, item.name || '', item.classID!);
877
+ writeClassStructure(writer, item._name || '', item._classID!);
900
878
  writeAsciiStringOrClassId(writer, item.keyID!);
901
879
  break;
902
880
  case 'Clss': // Class
903
- writeClassStructure(writer, item.name || '', item.classID!);
881
+ writeClassStructure(writer, item._name || '', item._classID!);
904
882
  break;
905
883
  case 'Enmr': // Enumerated Reference
906
- writeClassStructure(writer, item.name || '', item.classID!);
884
+ writeClassStructure(writer, item._name || '', item._classID!);
907
885
  writeAsciiStringOrClassId(writer, item.typeID!);
908
886
  writeAsciiStringOrClassId(writer, item.enumValue!);
909
887
  break;
910
888
  case 'rele': // Offset
911
- writeClassStructure(writer, item.name || '', item.classID!);
889
+ writeClassStructure(writer, item._name || '', item._classID!);
912
890
  writeUint32(writer, item.value as number);
913
891
  break;
914
892
  case 'Idnt': // Identifier
@@ -916,7 +894,7 @@ function writeReferenceStructure(writer: PsdWriter, _key: string, items: any[])
916
894
  writeInt32(writer, item.value as number);
917
895
  break;
918
896
  case 'name': // Name
919
- writeClassStructure(writer, item.name || '', item.classID!);
897
+ writeClassStructure(writer, item._name || '', item._classID!);
920
898
  writeUnicodeStringWithPadding(writer, `${item.value}`);
921
899
  break;
922
900
  default:
@@ -1006,8 +984,8 @@ export type DesciptorGradient = {
1006
984
  GrdF: 'GrdF.CstS';
1007
985
  Intr: number;
1008
986
  Clrs: {
1009
- 'Clr ': DescriptorColor;
1010
- Type: 'Clry.UsrS';
987
+ 'Clr '?: DescriptorColor;
988
+ Type?: string; // 'Clry.UsrS' | 'Clry.FrgC' | 'Clry.BckC'
1011
989
  Lctn: number;
1012
990
  Mdpn: number;
1013
991
  }[];
@@ -1047,7 +1025,9 @@ export interface DescriptorGradientContent {
1047
1025
  export interface DescriptorPatternContent {
1048
1026
  Ptrn: DesciptorPattern;
1049
1027
  Lnkd?: boolean;
1050
- phase?: { Hrzn: number; Vrtc: number; };
1028
+ phase?: { Hrzn: number | DescriptorUnitsValue; Vrtc: number | DescriptorUnitsValue; };
1029
+ 'Scl '?: DescriptorUnitsValue;
1030
+ Angl?: DescriptorUnitsValue;
1051
1031
  }
1052
1032
 
1053
1033
  export type DescriptorVectorContent = DescriptorColorContent | DescriptorGradientContent | DescriptorPatternContent;
@@ -1343,7 +1323,7 @@ function parseFxObject(fx: EffectDescriptor) {
1343
1323
  fillType: FrFl.decode(fx.PntT!),
1344
1324
  blendMode: BlnM.decode(fx['Md ']!),
1345
1325
  opacity: parsePercent(fx.Opct),
1346
- size: parseUnits(fx['Sz ']!),
1326
+ size: parseUnitsOrNumber(fx['Sz '] as DescriptorUnitsValue | number), // stored as number (pixels) in old versions
1347
1327
  };
1348
1328
 
1349
1329
  if (fx.present !== undefined) stroke.present = fx.present;
@@ -1428,16 +1408,16 @@ export function serializeEffects(e: LayerEffectsInfo, log: boolean, multi: boole
1428
1408
 
1429
1409
  export function parseEffects(info: Lfx2Descriptor & LmfxDescriptor, log: boolean) {
1430
1410
  const effects: LayerEffectsInfo = {};
1431
- const { masterFXSwitch, DrSh, dropShadowMulti, IrSh, innerShadowMulti, OrGl, IrGl, ebbl, SoFi, solidFillMulti, patternFill, GrFl, gradientFillMulti, ChFX, FrFX, frameFXMulti, numModifyingFX, ...rest } = info;
1411
+ const { _name, _classID, 'Scl ': scale, masterFXSwitch, DrSh, dropShadowMulti, IrSh, innerShadowMulti, OrGl, IrGl, ebbl, SoFi, solidFillMulti, patternFill, GrFl, gradientFillMulti, ChFX, FrFX, frameFXMulti, numModifyingFX, ...rest } = info as typeof info & { _name?: string; _classID?: string; };
1432
1412
  if (!masterFXSwitch) effects.disabled = true;
1433
- if (info['Scl ']) effects.scale = parsePercent(info['Scl ']);
1413
+ if (scale) effects.scale = parsePercent(scale);
1434
1414
  if (DrSh) effects.dropShadow = [parseEffectObject(DrSh, log)];
1435
1415
  if (dropShadowMulti) effects.dropShadow = dropShadowMulti.map(i => parseEffectObject(i, log));
1436
1416
  if (IrSh) effects.innerShadow = [parseEffectObject(IrSh, log)];
1437
1417
  if (innerShadowMulti) effects.innerShadow = innerShadowMulti.map(i => parseEffectObject(i, log));
1438
1418
  if (OrGl) effects.outerGlow = parseEffectObject(OrGl, log);
1439
1419
  if (IrGl) effects.innerGlow = parseEffectObject(IrGl, log);
1440
- if (ebbl) effects.bevel = parseEffectObject(ebbl, log);
1420
+ if (ebbl) effects.bevel = parseEffectObject(ebbl, log, 'bevel');
1441
1421
  if (SoFi) effects.solidFill = [parseEffectObject(SoFi, log)];
1442
1422
  if (solidFillMulti) effects.solidFill = solidFillMulti.map(i => parseEffectObject(i, log));
1443
1423
  if (patternFill) effects.patternOverlay = parseEffectObject(patternFill, log);
@@ -1446,7 +1426,7 @@ export function parseEffects(info: Lfx2Descriptor & LmfxDescriptor, log: boolean
1446
1426
  if (ChFX) effects.satin = parseEffectObject(ChFX, log);
1447
1427
  if (FrFX) effects.stroke = [parseFxObject(FrFX)];
1448
1428
  if (frameFXMulti) effects.stroke = frameFXMulti.map(i => parseFxObject(i));
1449
- if (log && Object.keys(rest).length > 1) console.log('Unhandled effect keys:', rest);
1429
+ if (log && Object.keys(rest).length) console.log('Unhandled effect keys:', rest);
1450
1430
  return effects;
1451
1431
  }
1452
1432
 
@@ -1581,7 +1561,7 @@ type AllEffects = LayerEffectShadow & LayerEffectsOuterGlow & LayerEffectStroke
1581
1561
  LayerEffectInnerGlow & LayerEffectBevel & LayerEffectSolidFill &
1582
1562
  LayerEffectPatternOverlay & LayerEffectSatin & LayerEffectGradientOverlay;
1583
1563
 
1584
- function parseEffectObject(obj: any, reportErrors: boolean) {
1564
+ function parseEffectObject(obj: any, reportErrors: boolean, objName?: string) {
1585
1565
  const result: AllEffects = {} as any;
1586
1566
 
1587
1567
  for (const key of Object.keys(obj)) {
@@ -1608,7 +1588,10 @@ function parseEffectObject(obj: any, reportErrors: boolean) {
1608
1588
  case 'GlwT': result.technique = BETE.decode(val) as any; break;
1609
1589
  case 'glwS': result.source = IGSr.decode(val); break;
1610
1590
  case 'Type': result.type = GrdT.decode(val); break;
1611
- case 'gs99': result.interpolationMethod = gradientInterpolationMethodType.decode(val); break;
1591
+ case 'gs99': // gradient overlay
1592
+ case 'gradientsInterpolationMethod': // glows
1593
+ result.interpolationMethod = gradientInterpolationMethodType.decode(val);
1594
+ break;
1612
1595
  case 'Opct': result.opacity = parsePercent(val); break;
1613
1596
  case 'hglO': result.highlightOpacity = parsePercent(val); break;
1614
1597
  case 'sdwO': result.shadowOpacity = parsePercent(val); break;
@@ -1625,15 +1608,23 @@ function parseEffectObject(obj: any, reportErrors: boolean) {
1625
1608
  case 'Dstn': result.distance = parseUnits(val); break;
1626
1609
  case 'Scl ': result.scale = parsePercent(val); break;
1627
1610
  case 'Ptrn': result.pattern = { name: val['Nm '], id: val.Idnt }; break;
1628
- case 'phase': result.phase = { x: val.Hrzn, y: val.Vrtc }; break;
1611
+ case 'phase': result.phase = parsePhase(val); break;
1629
1612
  case 'Ofst': result.offset = { x: parsePercent(val.Hrzn), y: parsePercent(val.Vrtc) }; break;
1630
1613
  case 'MpgS':
1631
- case 'TrnS':
1632
- result.contour = {
1614
+ case 'TrnS': {
1615
+ const contour: EffectContour = {
1633
1616
  name: val['Nm '],
1634
1617
  curve: (val['Crv '] as any[]).map(p => ({ x: p.Hrzn, y: p.Vrtc })),
1635
1618
  };
1619
+
1620
+ // bevel can have both: gloss contour (TrnS) and contour of the shape (MpgS)
1621
+ if (objName === 'bevel' && key === 'MpgS') {
1622
+ result.shapeContour = contour;
1623
+ } else {
1624
+ result.contour = contour;
1625
+ }
1636
1626
  break;
1627
+ }
1637
1628
  case 'Grad': result.gradient = parseGradient(val); break;
1638
1629
  case 'useTexture':
1639
1630
  case 'useShape':
@@ -1691,7 +1682,9 @@ function serializeEffectObject(obj: any, objName: string, reportErrors: boolean)
1691
1682
  break;
1692
1683
  case 'source': result.glwS = IGSr.encode(val); break;
1693
1684
  case 'type': result.Type = GrdT.encode(val); break;
1694
- case 'interpolationMethod': result.gs99 = gradientInterpolationMethodType.encode(val); break;
1685
+ case 'interpolationMethod':
1686
+ result[(objName === 'outerGlow' || objName === 'innerGlow') ? 'gradientsInterpolationMethod' : 'gs99'] = gradientInterpolationMethodType.encode(val);
1687
+ break;
1695
1688
  case 'opacity': result.Opct = unitsPercent(val); break;
1696
1689
  case 'highlightOpacity': result.hglO = unitsPercent(val); break;
1697
1690
  case 'shadowOpacity': result.sdwO = unitsPercent(val); break;
@@ -1714,14 +1707,9 @@ function serializeEffectObject(obj: any, objName: string, reportErrors: boolean)
1714
1707
  case 'scale': result['Scl '] = unitsPercent(val); break;
1715
1708
  case 'pattern': result.Ptrn = { 'Nm ': val.name, Idnt: val.id }; break;
1716
1709
  case 'phase': result.phase = { Hrzn: val.x, Vrtc: val.y }; break;
1717
- case 'offset': result.Ofst = { Hrzn: unitsPercent(val.x), Vrtc: unitsPercent(val.y) }; break;
1718
- case 'contour': {
1719
- result[objName === 'satin' ? 'MpgS' : 'TrnS'] = {
1720
- 'Nm ': (val as EffectContour).name,
1721
- 'Crv ': (val as EffectContour).curve.map(p => ({ Hrzn: p.x, Vrtc: p.y })),
1722
- };
1723
- break;
1724
- }
1710
+ case 'offset': result.Ofst = { Hrzn: unitsPercentF(val.x), Vrtc: unitsPercentF(val.y) }; break;
1711
+ case 'contour': result[objName === 'satin' ? 'MpgS' : 'TrnS'] = serializeContour(val); break;
1712
+ case 'shapeContour': result.MpgS = serializeContour(val); break;
1725
1713
  case 'gradient': result.Grad = serializeGradient(val); break;
1726
1714
  case 'useTexture':
1727
1715
  case 'useShape':
@@ -1739,6 +1727,13 @@ function serializeEffectObject(obj: any, objName: string, reportErrors: boolean)
1739
1727
  return result;
1740
1728
  }
1741
1729
 
1730
+ function serializeContour(contour: EffectContour) {
1731
+ return {
1732
+ 'Nm ': contour.name,
1733
+ 'Crv ': contour.curve.map(p => ({ Hrzn: p.x, Vrtc: p.y })),
1734
+ };
1735
+ }
1736
+
1742
1737
  function parseGradient(grad: DesciptorGradient): EffectSolidGradient | EffectNoiseGradient {
1743
1738
  if (grad.GrdF === 'GrdF.CstS') {
1744
1739
  // stop locations are always in 0..4096 range, independent of smoothness (Intr)
@@ -1748,11 +1743,22 @@ function parseGradient(grad: DesciptorGradient): EffectSolidGradient | EffectNoi
1748
1743
  type: 'solid',
1749
1744
  name: grad['Nm '],
1750
1745
  smoothness: grad.Intr / 4096,
1751
- colorStops: grad.Clrs.map(s => ({
1752
- color: parseColor(s['Clr ']),
1753
- location: s.Lctn / samples,
1754
- midpoint: s.Mdpn / 100,
1755
- })),
1746
+ colorStops: grad.Clrs.map(s => {
1747
+ const stop: ColorStop = {
1748
+ color: s['Clr '] ? parseColor(s['Clr ']) : { r: 0, g: 0, b: 0 },
1749
+ location: s.Lctn / samples,
1750
+ midpoint: s.Mdpn / 100,
1751
+ };
1752
+
1753
+ if (s.Type) {
1754
+ try {
1755
+ const type = Clry.decode(s.Type);
1756
+ if (type !== 'user') stop.type = type;
1757
+ } catch { } // unknown stop type, read as user color
1758
+ }
1759
+
1760
+ return stop;
1761
+ }),
1756
1762
  opacityStops: grad.Trns.map(s => ({
1757
1763
  opacity: parsePercent(s.Opct),
1758
1764
  location: s.Lctn / samples,
@@ -1784,12 +1790,12 @@ function serializeGradient(grad: EffectSolidGradient | EffectNoiseGradient): Des
1784
1790
  Intr: Math.round((grad.smoothness ?? 1) * 4096),
1785
1791
  Clrs: grad.colorStops.map(s => ({
1786
1792
  'Clr ': serializeColor(s.color),
1787
- Type: 'Clry.UsrS',
1793
+ Type: Clry.encode(s.type ?? 'user'),
1788
1794
  Lctn: Math.round(s.location * samples),
1789
1795
  Mdpn: Math.round((s.midpoint ?? 0.5) * 100),
1790
1796
  })),
1791
1797
  Trns: grad.opacityStops.map(s => ({
1792
- Opct: unitsPercent(s.opacity),
1798
+ Opct: unitsPercentF(s.opacity),
1793
1799
  Lctn: Math.round(s.location * samples),
1794
1800
  Mdpn: Math.round((s.midpoint ?? 0.5) * 100),
1795
1801
  })),
@@ -1827,13 +1833,23 @@ function parseGradientContent(descriptor: DescriptorGradientContent) {
1827
1833
  return result;
1828
1834
  }
1829
1835
 
1836
+ // phase is usually stored as plain numbers, but some files store it as unit values
1837
+ function parsePhase({ Hrzn, Vrtc }: { Hrzn: number | DescriptorUnitsValue; Vrtc: number | DescriptorUnitsValue; }) {
1838
+ return {
1839
+ x: typeof Hrzn === 'number' ? Hrzn : Hrzn.value,
1840
+ y: typeof Vrtc === 'number' ? Vrtc : Vrtc.value,
1841
+ };
1842
+ }
1843
+
1830
1844
  function parsePatternContent(descriptor: DescriptorPatternContent) {
1831
1845
  const result: EffectPattern & ExtraPatternInfo = {
1832
1846
  name: descriptor.Ptrn['Nm '],
1833
1847
  id: descriptor.Ptrn.Idnt,
1834
1848
  };
1835
1849
  if (descriptor.Lnkd !== undefined) result.linked = descriptor.Lnkd;
1836
- if (descriptor.phase !== undefined) result.phase = { x: descriptor.phase.Hrzn, y: descriptor.phase.Vrtc };
1850
+ if (descriptor.phase !== undefined) result.phase = parsePhase(descriptor.phase);
1851
+ if (descriptor['Scl '] !== undefined) result.scale = parsePercent(descriptor['Scl ']);
1852
+ if (descriptor.Angl !== undefined) result.angle = parseAngle(descriptor.Angl);
1837
1853
  return result;
1838
1854
  }
1839
1855
 
@@ -1858,11 +1874,11 @@ function serializeGradientContent(content: (EffectSolidGradient | EffectNoiseGra
1858
1874
  if (content.angle !== undefined) result.Angl = unitsAngle(content.angle);
1859
1875
  result.Type = GrdT.encode(content.style);
1860
1876
  if (content.align !== undefined) result.Algn = content.align;
1861
- if (content.scale !== undefined) result['Scl '] = unitsPercent(content.scale);
1877
+ if (content.scale !== undefined) result['Scl '] = unitsPercentF(content.scale);
1862
1878
  if (content.offset) {
1863
1879
  result.Ofst = {
1864
- Hrzn: unitsPercent(content.offset.x),
1865
- Vrtc: unitsPercent(content.offset.y),
1880
+ Hrzn: unitsPercentF(content.offset.x),
1881
+ Vrtc: unitsPercentF(content.offset.y),
1866
1882
  };
1867
1883
  }
1868
1884
  result.Grad = serializeGradient(content);
@@ -1878,6 +1894,8 @@ function serializePatternContent(content: EffectPattern & ExtraPatternInfo) {
1878
1894
  };
1879
1895
  if (content.linked !== undefined) result.Lnkd = !!content.linked;
1880
1896
  if (content.phase !== undefined) result.phase = { Hrzn: content.phase.x, Vrtc: content.phase.y };
1897
+ if (content.scale !== undefined) result['Scl '] = unitsPercentF(content.scale);
1898
+ if (content.angle !== undefined) result.Angl = unitsAngle(content.angle);
1881
1899
  return result;
1882
1900
  }
1883
1901
 
@@ -1894,10 +1912,9 @@ export function serializeVectorContent(content: VectorContent): { descriptor: De
1894
1912
  // Descriptor color values: HSB saturation/brightness 0..100, CMYK 0..100, Gray 0..100, Lab L 0..100, a/b -128..127
1895
1913
  // Color values (see psd.ts): HSB 0..1, CMYK 0..255, Gray 0..255, Lab L 0..1, a/b -1..1 (same as readColor/writeColor)
1896
1914
 
1897
- const colorSourceKey = typeof Symbol !== 'undefined' ? Symbol('agPsdColorSource') : '__agPsdColorSource';
1898
-
1899
1915
  const colorConversions: { [key: string]: ((x: number) => number)[] | undefined; } = {
1900
1916
  // key: [descriptor -> color, color -> descriptor]
1917
+ 'H ': [x => x / 360, x => x * 360], // angle
1901
1918
  Strt: [x => x / 100, x => x * 100],
1902
1919
  Brgh: [x => x / 100, x => x * 100],
1903
1920
  'Cyn ': [x => x * 255 / 100, x => x * 100 / 255],
@@ -1926,41 +1943,36 @@ function colorToDescriptorValue(key: string, value: number) {
1926
1943
  return result;
1927
1944
  }
1928
1945
 
1929
- function shallowEqual(a: any, b: any) {
1930
- const keys = Object.keys(a);
1931
- if (keys.length !== Object.keys(b).length) return false;
1932
- return keys.every(key => a[key] === b[key]);
1946
+ function isBookColor(color: any) {
1947
+ return color._classID === 'BkCl' || 'Bk ' in color || 'bookID' in color;
1933
1948
  }
1934
1949
 
1935
1950
  export function parseColor(color: DescriptorColor): Color {
1936
- let result: Color;
1937
-
1938
1951
  if ('H ' in color) {
1939
- result = { h: parsePercentOrAngle(color['H ']), s: color.Strt / 100, b: color.Brgh / 100 };
1952
+ return { h: parsePercentOrAngle(color['H ']), s: color.Strt / 100, b: color.Brgh / 100 };
1940
1953
  } else if ('Rd ' in color) {
1941
1954
  return { r: color['Rd '], g: color['Grn '], b: color['Bl '] };
1942
1955
  } else if ('Cyn ' in color) {
1943
- result = {
1956
+ return {
1944
1957
  c: color['Cyn '] * 255 / 100,
1945
1958
  m: color.Mgnt * 255 / 100,
1946
1959
  y: color['Ylw '] * 255 / 100,
1947
1960
  k: color.Blck * 255 / 100,
1948
1961
  };
1949
1962
  } else if ('Gry ' in color) {
1950
- result = { k: color['Gry '] * 255 / 100 };
1963
+ return { k: color['Gry '] * 255 / 100 };
1951
1964
  } else if ('Lmnc' in color) {
1952
- result = { l: color.Lmnc / 100, a: normalizeLabAB(color['A ']), b: normalizeLabAB(color['B ']) };
1965
+ return { l: color.Lmnc / 100, a: normalizeLabAB(color['A ']), b: normalizeLabAB(color['B ']) };
1953
1966
  } else if ('redFloat' in color) {
1954
1967
  return { fr: color.redFloat, fg: color.greenFloat, fb: color.blueFloat };
1968
+ } else if (isBookColor(color)) {
1969
+ return { r: 0, g: 0, b: 0 }; // book (Pantone) colors are not supported
1955
1970
  } else {
1956
1971
  throw new Error('Unsupported color descriptor');
1957
1972
  }
1958
-
1959
- // remember original descriptor values, so unmodified colors can be written back without conversion errors
1960
- return setHidden(result, colorSourceKey, { color: { ...result }, descriptor: color });
1961
1973
  }
1962
1974
 
1963
- function serializeColorValues(color: Color | undefined): DescriptorColor {
1975
+ export function serializeColor(color: Color | undefined): DescriptorColor {
1964
1976
  if (!color) {
1965
1977
  return { _name: '', _classID: 'RGBC', 'Rd ': 0, 'Grn ': 0, 'Bl ': 0 };
1966
1978
  } else if ('r' in color) {
@@ -1970,7 +1982,7 @@ function serializeColorValues(color: Color | undefined): DescriptorColor {
1970
1982
  } else if ('h' in color) {
1971
1983
  return {
1972
1984
  _name: '', _classID: 'HSBC',
1973
- 'H ': unitsAngle((color.h || 0) * 360),
1985
+ 'H ': unitsAngle(colorToDescriptorValue('H ', color.h || 0)),
1974
1986
  Strt: colorToDescriptorValue('Strt', color.s || 0),
1975
1987
  Brgh: colorToDescriptorValue('Brgh', color.b || 0),
1976
1988
  };
@@ -1996,22 +2008,6 @@ function serializeColorValues(color: Color | undefined): DescriptorColor {
1996
2008
  }
1997
2009
  }
1998
2010
 
1999
- export function serializeColor(color: Color | undefined): DescriptorColor {
2000
- const result: any = serializeColorValues(color);
2001
- const source = getHidden(color, colorSourceKey);
2002
-
2003
- if (source && shallowEqual(source.color, color)) {
2004
- // color was not modified since it was read, use exact original values
2005
- for (const key of Object.keys(result)) {
2006
- if (colorConversions[key] && typeof source.descriptor[key] === 'number') {
2007
- result[key] = source.descriptor[key];
2008
- }
2009
- }
2010
- }
2011
-
2012
- return result;
2013
- }
2014
-
2015
2011
  export function parseAngle(x: DescriptorUnitsValue) {
2016
2012
  if (x === undefined) return 0;
2017
2013
  if (x.units !== 'Angle') throw new Error(`Invalid units: ${x.units}`);
@@ -2034,7 +2030,8 @@ export function parsePercentOrAngle(x: DescriptorUnitsValue | undefined) {
2034
2030
  export function parseUnits({ units, value }: DescriptorUnitsValue): UnitsValue {
2035
2031
  if (
2036
2032
  units !== 'Pixels' && units !== 'Millimeters' && units !== 'Points' && units !== 'None' &&
2037
- units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density'
2033
+ units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density' &&
2034
+ units !== 'Distance'
2038
2035
  ) {
2039
2036
  throw new Error(`Invalid units: ${JSON.stringify({ units, value })}`);
2040
2037
  }
@@ -2076,7 +2073,8 @@ export function unitsValue(x: UnitsValue | undefined, key: string): DescriptorUn
2076
2073
 
2077
2074
  if (
2078
2075
  units !== 'Pixels' && units !== 'Millimeters' && units !== 'Points' && units !== 'None' &&
2079
- units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density'
2076
+ units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density' &&
2077
+ units !== 'Distance'
2080
2078
  ) {
2081
2079
  throw new Error(`Invalid units in ${JSON.stringify(x)} (key: ${key})`);
2082
2080
  }
@@ -2222,6 +2220,12 @@ export const gradientInterpolationMethodType = createEnum<InterpolationMethod>('
2222
2220
  // TODO: stripes
2223
2221
  });
2224
2222
 
2223
+ export const Clry = createEnum<'user' | 'foreground' | 'background'>('Clry', 'user', {
2224
+ user: 'UsrS',
2225
+ foreground: 'FrgC',
2226
+ background: 'BckC',
2227
+ });
2228
+
2225
2229
  export const ClrS = createEnum<'rgb' | 'hsb' | 'lab' | 'hsl'>('ClrS', 'rgb', {
2226
2230
  rgb: 'RGBC',
2227
2231
  hsb: 'HSBl',