ag-psd 32.0.0 → 34.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  22. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  23. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  24. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  25. package/CHANGELOG.md +100 -0
  26. package/LICENSE +5 -0
  27. package/README.md +4 -7
  28. package/README_PSD.md +261 -84
  29. package/TODO.md +13 -84
  30. package/dist/abr.d.ts +11 -4
  31. package/dist/abr.js +362 -230
  32. package/dist/abr.js.map +1 -1
  33. package/dist/additionalInfo.d.ts +56 -2
  34. package/dist/additionalInfo.js +828 -361
  35. package/dist/additionalInfo.js.map +1 -1
  36. package/dist/ase.d.ts +6 -1
  37. package/dist/ase.js +61 -61
  38. package/dist/ase.js.map +1 -1
  39. package/dist/bundle.js +3321 -1364
  40. package/dist/descriptor.d.ts +28 -13
  41. package/dist/descriptor.js +295 -241
  42. package/dist/descriptor.js.map +1 -1
  43. package/dist/helpers.d.ts +7 -1
  44. package/dist/helpers.js +246 -4
  45. package/dist/helpers.js.map +1 -1
  46. package/dist/imageResources.d.ts +15 -3
  47. package/dist/imageResources.js +586 -144
  48. package/dist/imageResources.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/jpeg.js +8 -8
  53. package/dist/jpeg.js.map +1 -1
  54. package/dist/psd.d.ts +292 -19
  55. package/dist/psd.js.map +1 -1
  56. package/dist/psdReader.d.ts +10 -0
  57. package/dist/psdReader.js +424 -216
  58. package/dist/psdReader.js.map +1 -1
  59. package/dist/psdWriter.d.ts +2 -0
  60. package/dist/psdWriter.js +373 -113
  61. package/dist/psdWriter.js.map +1 -1
  62. package/dist/text.d.ts +32 -1
  63. package/dist/text.js +75 -6
  64. package/dist/text.js.map +1 -1
  65. package/dist/utf8.js +0 -3
  66. package/dist/utf8.js.map +1 -1
  67. package/dist-es/abr.d.ts +11 -4
  68. package/dist-es/abr.js +362 -230
  69. package/dist-es/abr.js.map +1 -1
  70. package/dist-es/additionalInfo.d.ts +56 -2
  71. package/dist-es/additionalInfo.js +828 -361
  72. package/dist-es/additionalInfo.js.map +1 -1
  73. package/dist-es/ase.d.ts +6 -1
  74. package/dist-es/ase.js +61 -61
  75. package/dist-es/ase.js.map +1 -1
  76. package/dist-es/descriptor.d.ts +28 -13
  77. package/dist-es/descriptor.js +295 -241
  78. package/dist-es/descriptor.js.map +1 -1
  79. package/dist-es/helpers.d.ts +7 -1
  80. package/dist-es/helpers.js +246 -4
  81. package/dist-es/helpers.js.map +1 -1
  82. package/dist-es/imageResources.d.ts +15 -3
  83. package/dist-es/imageResources.js +586 -144
  84. package/dist-es/imageResources.js.map +1 -1
  85. package/dist-es/index.d.ts +1 -0
  86. package/dist-es/index.js +1 -0
  87. package/dist-es/index.js.map +1 -1
  88. package/dist-es/jpeg.js +8 -8
  89. package/dist-es/jpeg.js.map +1 -1
  90. package/dist-es/psd.d.ts +292 -19
  91. package/dist-es/psd.js.map +1 -1
  92. package/dist-es/psdReader.d.ts +10 -0
  93. package/dist-es/psdReader.js +424 -216
  94. package/dist-es/psdReader.js.map +1 -1
  95. package/dist-es/psdWriter.d.ts +2 -0
  96. package/dist-es/psdWriter.js +373 -113
  97. package/dist-es/psdWriter.js.map +1 -1
  98. package/dist-es/text.d.ts +32 -1
  99. package/dist-es/text.js +75 -6
  100. package/dist-es/text.js.map +1 -1
  101. package/dist-es/utf8.js +0 -3
  102. package/dist-es/utf8.js.map +1 -1
  103. package/package.json +2 -2
  104. package/perf.js +312 -9
  105. package/src/abr.ts +382 -235
  106. package/src/additionalInfo.ts +1071 -482
  107. package/src/ase.ts +66 -61
  108. package/src/descriptor.ts +291 -242
  109. package/src/helpers.ts +266 -3
  110. package/src/imageResources.ts +736 -169
  111. package/src/index.ts +1 -0
  112. package/src/jpeg.ts +8 -8
  113. package/src/psd.ts +271 -32
  114. package/src/psdReader.ts +440 -221
  115. package/src/psdWriter.ts +402 -110
  116. package/src/text.ts +93 -7
  117. package/src/utf8.ts +0 -4
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,
@@ -44,11 +44,6 @@ const unitsMap: Dict = {
44
44
  };
45
45
 
46
46
  const unitsMapRev = revMap(unitsMap);
47
- let logErrors = false;
48
-
49
- export function setLogErrors(value: boolean) {
50
- logErrors = value;
51
- }
52
47
 
53
48
  function makeType(name: string, classID: string) {
54
49
  return { name, classID };
@@ -98,6 +93,7 @@ const fieldToExtType: ExtTypeDict = {
98
93
  psVersion: nullType,
99
94
  docDefaultNewArtboardBackgroundColor: makeType('', 'RGBC'),
100
95
  artboardRect: makeType('', 'classFloatRect'),
96
+ framedGroupRect: makeType('', 'classFloatRect'),
101
97
  keyOriginRRectRadii: makeType('', 'radii'),
102
98
  keyOriginBoxCorners: nullType,
103
99
  rectangleCornerA: makeType('', 'Pnt '),
@@ -149,6 +145,7 @@ const fieldToArrayExtType: ExtTypeDict = {
149
145
  Clrs: makeType('', 'Clrt'),
150
146
  Trns: makeType('', 'TrnS'),
151
147
  keyDescriptorList: nullType,
148
+ keyRootDescriptorList: nullType,
152
149
  solidFillMulti: makeType('', 'SoFi'),
153
150
  gradientFillMulti: makeType('', 'GrFl'),
154
151
  dropShadowMulti: makeType('', 'DrSh'),
@@ -183,17 +180,23 @@ const fieldToArrayExtType: ExtTypeDict = {
183
180
 
184
181
  const typeToField: { [key: string]: string[]; } = {
185
182
  'TEXT': [
186
- 'Txt ', 'printerName', 'Nm ', 'Idnt', 'blackAndWhitePresetFileName', 'LUT3DFileName',
183
+ 'Txt ', 'printerName', 'Nm ', 'Idnt', 'blackAndWhitePresetFileName', 'LUT3DFileName', 'exportOption', 'scaleSettings',
184
+ 'BackgroundImagePath', 'ImageSubfolderName', 'SavedIteratedName',
187
185
  'presetFileName', 'curvesPresetFileName', 'mixerPresetFileName', 'placed', 'description', 'reason',
188
186
  'artboardPresetName', 'json', 'clipID', 'relPath', 'fullPath', 'mediaDescriptor', 'Msge',
189
- 'altTag', 'url', 'cellText', 'preset', 'KnNm', 'FPth', 'comment', 'originalPath',
187
+ 'altTag', 'url', 'cellText', 'preset', 'KnNm', 'FPth', 'comment', 'originalPath', 'Bk ', 'pathUnicodeName',
190
188
  ],
191
189
  'tdta': [
192
190
  'EngineData', 'LUT3DFileData', 'indexArray', 'originalVertexArray', 'deformedVertexArray',
193
- 'LqMe',
191
+ 'LqMe', 'bookKey',
194
192
  ],
195
193
  'long': [
196
- 'TextIndex', 'RndS', 'Mdpn', 'Smth', 'Lctn', 'strokeStyleVersion', 'LaID', 'Vrsn', 'Cnt ',
194
+ 'TextIndex', 'RndS', 'Mdpn', 'Smth', 'Lctn', 'strokeStyleVersion', 'LaID', 'Vrsn', 'Cnt ', 'bookID',
195
+ 'cropRectBottom', 'cropRectLeft', 'cropRectRight', 'cropRectTop',
196
+ // save for web output settings
197
+ 'Version', 'BackgroundColorBlue', 'BackgroundColorGreen', 'BackgroundColorRed', 'BackgroundColorState', 'AttributeCase', 'Encoding',
198
+ 'DuplicateFileNameBehavior', 'ImageMapLocation', 'ImageMapType', 'Indent', 'LineEndings', 'SpacersEmptyCells',
199
+ 'SpacersHorizontal', 'SpacersVertical', 'StylesFormat', 'TDWidthHeight', 'TagCase', 'ColorPolicy', 'MetadataPolicy',
197
200
  'Brgh', 'Cntr', 'means', 'vibrance', 'Strt', 'bwPresetKind', 'comp', 'compID', 'originalCompID',
198
201
  'curvesPresetKind', 'mixerPresetKind', 'uOrder', 'vOrder', 'PgNm', 'totalPages', 'Crop',
199
202
  'numerator', 'denominator', 'frameCount', 'Annt', 'keyOriginType', 'unitValueQuadVersion',
@@ -205,7 +208,7 @@ const typeToField: { [key: string]: string[]; } = {
205
208
  'meshExpansion', 'meshRigidity', 'VrsM', 'VrsN', 'NmbG', 'WLMn', 'WLMx', 'AmMn', 'AmMx', 'SclH', 'SclV',
206
209
  'Lvl ', 'TlNm', 'TlOf', 'FlRs', 'Thsh', 'ShrS', 'ShrE', 'FlRs', 'Vrnc', 'Strg', 'ExtS', 'ExtD',
207
210
  'HrzS', 'VrtS', 'NmbR', 'EdgF', 'Ang1', 'Ang2', 'Ang3', 'Ang4', 'lastAppliedComp', 'capturedInfo',
208
- 'pixelSourceType',
211
+ 'pixelSourceType', 'framedGroupType',
209
212
  ],
210
213
  'enum': [
211
214
  'textGridding', 'Ornt', 'warpStyle', 'warpRotate', 'Inte', 'Bltn', 'ClrS', 'BlrQ',
@@ -215,7 +218,7 @@ const typeToField: { [key: string]: string[]; } = {
215
218
  'tableOrder', 'enableCompCore', 'enableCompCoreGPU', 'compCoreSupport', 'compCoreGPUSupport', 'Engn',
216
219
  'enableCompCoreThreads', 'gs99', 'FrDs', 'trackID', 'animInterpStyle', 'horzAlign',
217
220
  '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',
221
+ 'IntC', 'Cnvr', 'Fl ', 'Dstr', 'MztT', 'Lns ', 'ExtT', 'DspM', 'ExtR', 'ZZTy', 'SphM', 'SmBQ', 'placedLayerOCIOConversion', 'gradientsInterpolationMethod', 'pathSymmetryMode',
219
222
  'interpretAlpha',
220
223
  ],
221
224
  'bool': [
@@ -229,15 +232,15 @@ const typeToField: { [key: string]: string[]; } = {
229
232
  'materialsDisclosed', 'hasMotion', 'muted', 'Effc', 'selected', 'autoScope', 'fillCanvas',
230
233
  'cellTextIsHTML', 'Smoo', 'Clsp', 'validAtPosition', 'rigidType', 'hasoptions', 'filterMaskEnable',
231
234
  'filterMaskLinked', 'filterMaskExtendWithWhite', 'removeJPEGArtifact', 'Mnch', 'ExtF', 'ExtM',
232
- 'moreAccurate', 'GpuY', 'LIWy', 'Cnty', 'Lnkd', 'showAlteredVideo', 'lightingOn',
235
+ 'moreAccurate', 'GpuY', 'LIWy', 'Cnty', 'Lnkd', 'showAlteredVideo', 'lightingOn', 'LockPPI',
233
236
  ],
234
237
  'doub': [
235
- 'warpValue', 'warpPerspective', 'warpPerspectiveOther', 'Intr', 'Wdth', 'Hght',
238
+ 'warpValue', 'warpPerspective', 'warpPerspectiveOther', 'Intr', 'Wdth', 'Hght', 'Gmm ',
236
239
  'strokeStyleMiterLimit', 'strokeStyleResolution', 'layerTime', 'keyOriginResolution',
237
240
  'xx', 'xy', 'yx', 'yy', 'tx', 'ty', 'FrGA', 'frameRate', 'audioLevel', 'rotation',
238
241
  'X ', 'Y ', 'redFloat', 'greenFloat', 'blueFloat', 'imageResolution',
239
242
  'PuX0', 'PuX1', 'PuX2', 'PuX3', 'PuY0', 'PuY1', 'PuY2', 'PuY3',
240
- 'stylization', 'cleanliness', 'brushScale', 'microBrush', 'specularity',
243
+ 'stylization', 'cleanliness', 'brushScale', 'microBrush', 'specularity', 'PreferredResolution',
241
244
  ],
242
245
  'UntF': [
243
246
  'sdwO', 'hglO', 'lagl', 'Lald', 'srgR', 'blur', 'Sftn', 'Opct', 'Dstn', 'Angl',
@@ -246,7 +249,7 @@ const typeToField: { [key: string]: string[]; } = {
246
249
  'topRight', 'topLeft', 'bottomLeft', 'bottomRight', 'ClNs', 'Shrp',
247
250
  ],
248
251
  'VlLs': [
249
- 'Crv ', 'Clrs', 'Mnm ', 'Mxm ', 'Trns', 'pathList', 'strokeStyleLineDashSet', 'FrLs', 'slices',
252
+ 'Crv ', 'Clrs', 'Mnm ', 'Mxm ', 'Trns', 'pathList', 'keyRootDescriptorList', 'strokeStyleLineDashSet', 'FrLs', 'slices',
250
253
  'LaSt', 'Trnf', 'nonAffineTransform', 'keyDescriptorList', 'guideIndeces', 'gradientFillMulti',
251
254
  'solidFillMulti', 'frameFXMulti', 'innerShadowMulti', 'dropShadowMulti', 'FrIn', 'FSts', 'FsFr',
252
255
  'sheetTimelineOptions', 'audioClipList', 'trackList', 'globalTrackList', 'keyList', 'audioClipList',
@@ -268,53 +271,45 @@ const channels = [
268
271
  // color classes whose channel values are written as doubles ('GRYC' and 'LABC' are kept for backwards compatibility)
269
272
  const doubleColorClasses = ['Grsc', 'LbCl', 'GRYC', 'LABC'];
270
273
 
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
- }
274
+ // fallback for unknown keys, based on the type of the value
275
+ export const cameraRawFilterClassID = 'Adobe Camera Raw Filter';
285
276
 
286
- const osTypeFamilies: { [key: string]: string; } = {
287
- UntF: 'UntF', UnFl: 'UntF',
288
- Objc: 'Objc', GlbO: 'Objc',
289
- type: 'type', GlbC: 'type',
290
- comp: 'comp',
277
+ // types of Camera Raw Filter settings (keys clash with other descriptors so they're typed by class)
278
+ export const cameraRawFilterKeyTypes: Dict = {
279
+ CrVe: 'TEXT', PrVN: 'long', PrVe: 'long', WBal: 'enum', Ex12: 'doub', ShpR: 'doub',
280
+ 'Crv ': 'VlLs', CrvR: 'VlLs', CrvG: 'VlLs', CrvB: 'VlLs',
291
281
  };
292
282
 
293
- // returns the OS type the value was originally read with, if it's compatible with the type we'd write it as
294
- // fallback for unknown keys, based on the type of the value
295
283
  function guessOSType(value: any): string | undefined {
296
284
  if (typeof value === 'boolean') return 'bool';
297
285
  if (typeof value === 'number' && !Number.isInteger(value)) return 'doub';
298
286
  if (Array.isArray(value)) return 'VlLs';
299
- if (value && typeof value === 'object' && typeof value._classID === 'string') return 'Objc';
300
- return undefined;
301
- }
302
287
 
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;
288
+ if (value && typeof value === 'object') {
289
+ if (typeof value._classID === 'string') return 'Objc';
290
+ if (typeof value.classID === 'string') return 'type'; // class read from 'type' or 'GlbC'
291
+ if (typeof value.low === 'number' && typeof value.high === 'number') return 'comp';
292
+ if (typeof value.units === 'string' && typeof value.value === 'number') return 'UntF';
293
+ }
294
+
295
+ return undefined;
308
296
  }
309
297
 
310
298
  const fieldToArrayType: Dict = {
311
299
  'Mnm ': 'long',
312
300
  'Mxm ': 'long',
301
+ HtmlFileNameComponents: 'long', // save for web output settings
302
+ SavingFileNameComponents: 'long',
303
+ SliceFileNameComponents: 'long',
304
+ Inpt: 'long', // levels filter
305
+ Otpt: 'long', // levels filter
313
306
  FrLs: 'long',
314
307
  strokeStyleLineDashSet: 'UntF',
315
308
  Trnf: 'doub',
316
309
  nonAffineTransform: 'doub',
317
310
  keyDescriptorList: 'Objc',
311
+ keyRootDescriptorList: 'Objc',
312
+ pathList: 'Objc',
318
313
  gradientFillMulti: 'Objc',
319
314
  solidFillMulti: 'Objc',
320
315
  frameFXMulti: 'Objc',
@@ -396,6 +391,12 @@ function getTypeByKey(key: string, value: any, root: string, parent: any) {
396
391
  return 'long';
397
392
  } else if ((key === 'Inpt' || key === 'Otpt') && parent._classID === 'HsbP') {
398
393
  return 'enum';
394
+ } else if ((key === 'Inpt' || key === 'Otpt') && parent._classID === 'LvlA') {
395
+ return 'VlLs';
396
+ } else if (key === 'Intr' && typeof value === 'boolean') {
397
+ return 'bool'; // interpolation in print style
398
+ } else if (key === 'PgPs') {
399
+ return 'enum';
399
400
  } else if (key === 'LghD' && parent._classID === 'oilPaint') {
400
401
  return 'long';
401
402
  } else {
@@ -409,7 +410,7 @@ export function readAsciiStringOrClassId(reader: PsdReader) {
409
410
  }
410
411
 
411
412
  export function writeAsciiStringOrClassId(writer: PsdWriter, value: string) {
412
- if (value.length === 4 && value !== 'warp' && value !== 'time' && value !== 'hold' && value !== 'list') {
413
+ if (value.length === 4 && value !== 'warp' && value !== 'time' && value !== 'hold' && value !== 'list' && value !== 'json') {
413
414
  // write classId
414
415
  writeInt32(writer, 0);
415
416
  writeSignature(writer, value);
@@ -441,12 +442,21 @@ export function readDescriptorStructure(reader: PsdReader, includeClass: boolean
441
442
  return object;
442
443
  }
443
444
 
445
+ export const outputSettingsRoot = 'outputSettings';
446
+
444
447
  export function writeDescriptorStructure(writer: PsdWriter, name: string, classId: string, value: any, root: string) {
445
- if (logErrors && !classId) console.log('Missing classId for: ', name, classId, value);
448
+ if (writer.logMissingFeatures && !classId) writer.log('Missing classId for: ', name, classId, value);
446
449
 
447
450
  // write class structure
448
451
  writeUnicodeStringWithPadding(writer, name);
449
- writeAsciiStringOrClassId(writer, classId);
452
+
453
+ if (root === outputSettingsRoot && classId === 'null') {
454
+ // Photoshop writes 'null' class id as 4 character string in save for web output settings
455
+ writeInt32(writer, 4);
456
+ writeSignature(writer, classId);
457
+ } else {
458
+ writeAsciiStringOrClassId(writer, classId);
459
+ }
450
460
 
451
461
  const allKeys = Object.keys(value);
452
462
  const keys = allKeys.filter(key => key !== '_name' && key !== '_classID' && value[key] !== undefined);
@@ -458,6 +468,11 @@ export function writeDescriptorStructure(writer: PsdWriter, name: string, classI
458
468
  let childRoot = root;
459
469
  let extType = fieldToExtType[key];
460
470
 
471
+ if (classId === cameraRawFilterClassID) {
472
+ type = cameraRawFilterKeyTypes[key] ?? 'long'; // other settings are integer levels
473
+ childRoot = cameraRawFilterClassID; // lists (curves) contain integer levels
474
+ }
475
+
461
476
  if (key === 'bounds' && root === 'text') {
462
477
  extType = makeType('', 'bounds');
463
478
  } else if (key === 'origin') {
@@ -527,24 +542,26 @@ export function writeDescriptorStructure(writer: PsdWriter, name: string, classI
527
542
  } else if (value[key].Ptrn) {
528
543
  extType = makeType('', 'patternLayer');
529
544
  } else {
530
- logErrors && console.log('Invalid strokeStyleContent value', value[key]);
545
+ writer.logMissingFeatures && writer.log('Invalid strokeStyleContent value', value[key]);
531
546
  }
532
547
  } else if (key === 'bounds' && root === 'quiltWarp') {
533
548
  extType = makeType('', 'classFloatRect');
534
549
  }
535
550
 
551
+ // guess color class from its fields (colors created by serializeColor always have _classID set)
536
552
  if (extType && extType.classID === 'RGBC') {
537
553
  if ('H ' in value[key]) extType = { classID: 'HSBC', name: '' };
538
- // TODO: other color spaces
554
+ else if ('Cyn ' in value[key]) extType = { classID: 'CMYC', name: '' };
555
+ else if ('Lmnc' in value[key]) extType = { classID: 'LbCl', name: '' };
556
+ else if ('Gry ' in value[key]) extType = { classID: 'Grsc', name: '' };
539
557
  }
540
558
 
541
- type = getRecordedOSType(value[key], type);
542
559
  if (!type) type = guessOSType(value[key]);
543
560
 
544
561
  writeAsciiStringOrClassId(writer, key);
545
562
  writeSignature(writer, type || 'long');
546
563
  writeOSType(writer, type || 'long', value[key], key, extType, childRoot);
547
- if (logErrors && !type) console.log(`Missing descriptor field type for: '${key}' in`, value);
564
+ if (writer.logMissingFeatures && !type) writer.log(`Missing descriptor field type for: '${key}' in`, value);
548
565
  }
549
566
  }
550
567
 
@@ -553,9 +570,8 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
553
570
  case 'obj ': // Reference
554
571
  return readReferenceStructure(reader);
555
572
  case 'Objc': // Descriptor
556
- return readDescriptorStructure(reader, includeClass);
557
573
  case 'GlbO': // GlobalObject same as Descriptor
558
- return setHidden(readDescriptorStructure(reader, includeClass), osTypeKey, type);
574
+ return readDescriptorStructure(reader, includeClass);
559
575
  case 'VlLs': { // List
560
576
  const length = readInt32(reader);
561
577
  const items: any[] = [];
@@ -580,7 +596,7 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
580
596
  const units = readSignature(reader);
581
597
  const value = readFloat32(reader);
582
598
  if (!unitsMap[units]) throw new Error(`Invalid units: ${units}`);
583
- return setHidden({ units: unitsMap[units], value }, osTypeKey, type);
599
+ return { units: unitsMap[units], value };
584
600
  }
585
601
  case 'TEXT': // String
586
602
  return readUnicodeString(reader);
@@ -594,13 +610,13 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
594
610
  case 'comp': { // Large Integer
595
611
  const low = readUint32(reader);
596
612
  const high = readUint32(reader);
597
- return setHidden({ low, high }, osTypeKey, type);
613
+ return { low, high };
598
614
  }
599
615
  case 'bool': // Boolean
600
616
  return !!readUint8(reader);
601
617
  case 'type': // Class
602
618
  case 'GlbC': // Class
603
- return setHidden(readClassStructure(reader), osTypeKey, type);
619
+ return readClassStructure(reader);
604
620
  case 'alis': { // Alias
605
621
  const length = readInt32(reader);
606
622
  return readAsciiString(reader, length);
@@ -612,24 +628,24 @@ function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
612
628
  case 'ObAr': { // Object array
613
629
  readInt32(reader); // version: 16
614
630
  readUnicodeString(reader); // name: ''
615
- const classID = readAsciiStringOrClassId(reader); // 'rationalPoint'
631
+ readAsciiStringOrClassId(reader); // classID: 'rationalPoint'
616
632
  const length = readInt32(reader);
617
633
  const items: any[] = [];
618
634
 
619
635
  for (let i = 0; i < length; i++) {
620
636
  const type1 = readAsciiStringOrClassId(reader); // type Hrzn | Vrtc
621
- const valueType = readSignature(reader); // UnFl
622
- const units = readSignature(reader); // units ? '#Pxl'
637
+ readSignature(reader); // UnFl
638
+ readSignature(reader); // units ? '#Pxl'
623
639
  const valuesCount = readInt32(reader);
624
640
  const values: number[] = [];
625
641
  for (let j = 0; j < valuesCount; j++) {
626
642
  values.push(readFloat64(reader));
627
643
  }
628
644
 
629
- items.push(setHidden({ type: type1, values }, obArInfoKey, { valueType, units }));
645
+ items.push({ type: type1, values });
630
646
  }
631
647
 
632
- return setHidden(items, obArInfoKey, { classID });
648
+ return items;
633
649
  }
634
650
  case 'Pth ': { // File path
635
651
  /*const length =*/ readInt32(reader); // total size of all fields below
@@ -669,10 +685,10 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
669
685
  writeInt32(writer, value.length);
670
686
 
671
687
  for (let i = 0; i < value.length; i++) {
672
- const type = getRecordedOSType(value[i], fieldToArrayType[key]) ?? guessOSType(value[i]);
688
+ const type = root === cameraRawFilterClassID ? 'long' : (fieldToArrayType[key] ?? guessOSType(value[i]));
673
689
  writeSignature(writer, type || 'long');
674
690
  writeOSType(writer, type || 'long', value[i], `${key}[]`, fieldToArrayExtType[key], root);
675
- if (logErrors && !type) console.log(`Missing descriptor array type for: '${key}' in`, value);
691
+ if (writer.logMissingFeatures && !type) writer.log(`Missing descriptor array type for: '${key}' in`, value);
676
692
  }
677
693
  break;
678
694
  case 'doub': // Double
@@ -733,16 +749,15 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
733
749
  case 'ObAr': { // Object array
734
750
  writeInt32(writer, 16); // version
735
751
  writeUnicodeStringWithPadding(writer, ''); // name
736
- const type = getHidden(value, obArInfoKey)?.classID ?? ObArTypes[key];
752
+ const type = ObArTypes[key];
737
753
  if (!type) throw new Error(`Not implemented ObArType for: ${key}`);
738
754
  writeAsciiStringOrClassId(writer, type);
739
755
  writeInt32(writer, value.length);
740
756
 
741
757
  for (let i = 0; i < value.length; i++) {
742
- const info = getHidden(value[i], obArInfoKey);
743
758
  writeAsciiStringOrClassId(writer, value[i].type); // Hrzn | Vrtc
744
- writeSignature(writer, info?.valueType ?? 'UnFl');
745
- writeSignature(writer, info?.units ?? '#Pxl');
759
+ writeSignature(writer, 'UnFl');
760
+ writeSignature(writer, '#Pxl');
746
761
  writeInt32(writer, value[i].values.length);
747
762
 
748
763
  for (let j = 0; j < value[i].values.length; j++) {
@@ -752,12 +767,13 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
752
767
  break;
753
768
  }
754
769
  case 'Pth ': { // File path
755
- const length = 4 + 4 + 4 + value.path.length * 2;
770
+ const path = value.path + '\0'; // Photoshop stores trailing null character (stripped when reading)
771
+ const length = 4 + 4 + 4 + path.length * 2;
756
772
  writeInt32(writer, length);
757
773
  writeSignature(writer, value.sig);
758
774
  writeInt32LE(writer, length);
759
- writeInt32LE(writer, value.path.length);
760
- writeUnicodeStringWithoutLengthLE(writer, value.path);
775
+ writeInt32LE(writer, path.length);
776
+ writeUnicodeStringWithoutLengthLE(writer, path);
761
777
  break;
762
778
  }
763
779
  default:
@@ -765,86 +781,64 @@ function writeOSType(writer: PsdWriter, type: string, value: any, key: string, e
765
781
  }
766
782
  }
767
783
 
784
+ // Reference item with explicit reference type and class, can be used in place of a value in reference arrays
785
+ // passed to writeVersionAndDescriptor (references are read as values only: strings, numbers and class objects)
768
786
  export interface DescriptorReferenceItem {
769
787
  type: 'prop' | 'Clss' | 'Enmr' | 'rele' | 'Idnt' | 'indx' | 'name';
770
- name?: string; // class name
771
- classID?: string;
788
+ _name?: string; // class name
789
+ _classID?: string;
772
790
  keyID?: string; // prop
773
791
  typeID?: string; // Enmr
774
792
  enumValue?: string; // Enmr
775
793
  value?: string | number; // rele, Idnt, indx, name
776
794
  }
777
795
 
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
- }
796
+ const referenceTypes = ['prop', 'Clss', 'Enmr', 'rele', 'Idnt', 'indx', 'name'];
795
797
 
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);
798
+ function isReferenceItem(value: any): value is DescriptorReferenceItem {
799
+ return !!value && typeof value === 'object' && referenceTypes.indexOf(value.type) !== -1;
800
800
  }
801
801
 
802
802
  function readReferenceStructure(reader: PsdReader) {
803
803
  const itemsCount = readInt32(reader);
804
- const infos: DescriptorReferenceItem[] = [];
804
+ const items: any[] = [];
805
805
 
806
806
  for (let i = 0; i < itemsCount; i++) {
807
807
  const type = readSignature(reader);
808
808
 
809
809
  switch (type) {
810
- case 'prop': { // Property
811
- const { name, classID } = readClassStructure(reader);
812
- const keyID = readAsciiStringOrClassId(reader);
813
- infos.push({ type, name, classID, keyID });
810
+ case 'prop': // Property
811
+ readClassStructure(reader);
812
+ items.push(readAsciiStringOrClassId(reader));
814
813
  break;
815
- }
816
- case 'Clss': { // Class
817
- const { name, classID } = readClassStructure(reader);
818
- infos.push({ type, name, classID });
814
+ case 'Clss': // Class
815
+ items.push(readClassStructure(reader));
819
816
  break;
820
- }
821
817
  case 'Enmr': { // Enumerated Reference
822
- const { name, classID } = readClassStructure(reader);
818
+ readClassStructure(reader);
823
819
  const typeID = readAsciiStringOrClassId(reader);
824
820
  const enumValue = readAsciiStringOrClassId(reader);
825
- infos.push({ type, name, classID, typeID, enumValue });
821
+ items.push(`${typeID}.${enumValue}`);
826
822
  break;
827
823
  }
828
- case 'rele': { // Offset
829
- const { name, classID } = readClassStructure(reader);
830
- infos.push({ type, name, classID, value: readUint32(reader) });
824
+ case 'rele': // Offset
825
+ readClassStructure(reader);
826
+ items.push(readUint32(reader));
831
827
  break;
832
- }
833
828
  case 'Idnt': // Identifier
834
829
  case 'indx': // Index
835
- infos.push({ type, value: readInt32(reader) });
830
+ items.push(readInt32(reader));
836
831
  break;
837
- case 'name': { // Name
838
- const { name, classID } = readClassStructure(reader);
839
- infos.push({ type, name, classID, value: readUnicodeString(reader) });
832
+ case 'name': // Name
833
+ readClassStructure(reader);
834
+ items.push(readUnicodeString(reader));
840
835
  break;
841
- }
842
836
  default:
843
837
  throw new Error(`Invalid descriptor reference type: ${type}`);
844
838
  }
845
839
  }
846
840
 
847
- return createDescriptorReference(infos);
841
+ return items;
848
842
  }
849
843
 
850
844
  function guessReferenceItem(value: any): DescriptorReferenceItem {
@@ -854,14 +848,14 @@ function guessReferenceItem(value: any): DescriptorReferenceItem {
854
848
  const match = /^([a-z ]{4}|[a-z]{5,})\.([a-z ]+)$/i.exec(value);
855
849
 
856
850
  if (match) {
857
- return { type: 'Enmr', name: '', classID: match[1], typeID: match[1], enumValue: match[2] };
851
+ return { type: 'Enmr', _name: '', _classID: match[1], typeID: match[1], enumValue: match[2] };
858
852
  } else {
859
- return { type: 'name', name: '', classID: 'Lyr ', value };
853
+ return { type: 'name', _name: '', _classID: 'Lyr ', value };
860
854
  }
861
855
  } else if (typeof value === 'number') {
862
856
  return { type: 'Idnt', value };
863
857
  } else if (value && typeof value === 'object' && typeof value.classID === 'string') {
864
- return { type: 'Clss', name: value.name || '', classID: value.classID };
858
+ return { type: 'Clss', _name: value.name || '', _classID: value.classID };
865
859
  } else {
866
860
  throw new Error(`Invalid descriptor reference value: ${JSON.stringify(value)}`);
867
861
  }
@@ -869,46 +863,39 @@ function guessReferenceItem(value: any): DescriptorReferenceItem {
869
863
 
870
864
  // path selection state (image resource 1088) is a reference to paths, enumerated references there use 'Path' class
871
865
  // (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
-
866
+ export function pathSelectionStateToReference(items: string[]): DescriptorReferenceItem[] {
867
+ return items.map(value => {
878
868
  const item = guessReferenceItem(value);
879
- if (item.type === 'Enmr') item.classID = 'Path';
869
+ if (item.type === 'Enmr') item._classID = 'Path';
880
870
  return item;
881
- }));
871
+ });
882
872
  }
883
873
 
884
874
  function writeReferenceStructure(writer: PsdWriter, _key: string, items: any[]) {
885
875
  if (!Array.isArray(items)) throw new Error(`Invalid reference value: ${JSON.stringify(items)}, key: ${_key}`);
886
876
 
887
- const infos: DescriptorReferenceItem[] | undefined = getHidden(items, referenceInfoKey);
888
877
  writeInt32(writer, items.length);
889
878
 
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);
879
+ for (const value of items) {
880
+ const item = isReferenceItem(value) ? value : guessReferenceItem(value);
894
881
 
895
882
  writeSignature(writer, item.type);
896
883
 
897
884
  switch (item.type) {
898
885
  case 'prop': // Property
899
- writeClassStructure(writer, item.name || '', item.classID!);
886
+ writeClassStructure(writer, item._name || '', item._classID!);
900
887
  writeAsciiStringOrClassId(writer, item.keyID!);
901
888
  break;
902
889
  case 'Clss': // Class
903
- writeClassStructure(writer, item.name || '', item.classID!);
890
+ writeClassStructure(writer, item._name || '', item._classID!);
904
891
  break;
905
892
  case 'Enmr': // Enumerated Reference
906
- writeClassStructure(writer, item.name || '', item.classID!);
893
+ writeClassStructure(writer, item._name || '', item._classID!);
907
894
  writeAsciiStringOrClassId(writer, item.typeID!);
908
895
  writeAsciiStringOrClassId(writer, item.enumValue!);
909
896
  break;
910
897
  case 'rele': // Offset
911
- writeClassStructure(writer, item.name || '', item.classID!);
898
+ writeClassStructure(writer, item._name || '', item._classID!);
912
899
  writeUint32(writer, item.value as number);
913
900
  break;
914
901
  case 'Idnt': // Identifier
@@ -916,7 +903,7 @@ function writeReferenceStructure(writer: PsdWriter, _key: string, items: any[])
916
903
  writeInt32(writer, item.value as number);
917
904
  break;
918
905
  case 'name': // Name
919
- writeClassStructure(writer, item.name || '', item.classID!);
906
+ writeClassStructure(writer, item._name || '', item._classID!);
920
907
  writeUnicodeStringWithPadding(writer, `${item.value}`);
921
908
  break;
922
909
  default:
@@ -1006,8 +993,8 @@ export type DesciptorGradient = {
1006
993
  GrdF: 'GrdF.CstS';
1007
994
  Intr: number;
1008
995
  Clrs: {
1009
- 'Clr ': DescriptorColor;
1010
- Type: 'Clry.UsrS';
996
+ 'Clr '?: DescriptorColor;
997
+ Type?: string; // 'Clry.UsrS' | 'Clry.FrgC' | 'Clry.BckC'
1011
998
  Lctn: number;
1012
999
  Mdpn: number;
1013
1000
  }[];
@@ -1047,7 +1034,9 @@ export interface DescriptorGradientContent {
1047
1034
  export interface DescriptorPatternContent {
1048
1035
  Ptrn: DesciptorPattern;
1049
1036
  Lnkd?: boolean;
1050
- phase?: { Hrzn: number; Vrtc: number; };
1037
+ phase?: { Hrzn: number | DescriptorUnitsValue; Vrtc: number | DescriptorUnitsValue; };
1038
+ 'Scl '?: DescriptorUnitsValue;
1039
+ Angl?: DescriptorUnitsValue;
1051
1040
  }
1052
1041
 
1053
1042
  export type DescriptorVectorContent = DescriptorColorContent | DescriptorGradientContent | DescriptorPatternContent;
@@ -1343,7 +1332,7 @@ function parseFxObject(fx: EffectDescriptor) {
1343
1332
  fillType: FrFl.decode(fx.PntT!),
1344
1333
  blendMode: BlnM.decode(fx['Md ']!),
1345
1334
  opacity: parsePercent(fx.Opct),
1346
- size: parseUnits(fx['Sz ']!),
1335
+ size: parseUnitsOrNumber(fx['Sz '] as DescriptorUnitsValue | number), // stored as number (pixels) in old versions
1347
1336
  };
1348
1337
 
1349
1338
  if (fx.present !== undefined) stroke.present = fx.present;
@@ -1373,7 +1362,7 @@ function serializeFxObject(stroke: LayerEffectStroke) {
1373
1362
  return FrFX;
1374
1363
  }
1375
1364
 
1376
- export function serializeEffects(e: LayerEffectsInfo, log: boolean, multi: boolean) {
1365
+ export function serializeEffects(e: LayerEffectsInfo, writer: PsdWriter, multi: boolean) {
1377
1366
  const info: Lfx2Descriptor & LmfxDescriptor = multi ? {
1378
1367
  'Scl ': unitsPercentF(e.scale ?? 1),
1379
1368
  masterFXSwitch: !e.disabled,
@@ -1390,20 +1379,20 @@ export function serializeEffects(e: LayerEffectsInfo, log: boolean, multi: boole
1390
1379
  const useMulti = <T>(arr: undefined | T[]): arr is T[] => !!arr && arr.length > 1 && multi;
1391
1380
  const useSingle = <T>(arr: undefined | T[]): arr is T[] => !!arr && arr.length >= 1 && (!multi || arr.length === 1);
1392
1381
 
1393
- if (useSingle(e.dropShadow)) info.DrSh = serializeEffectObject(e.dropShadow[0], 'dropShadow', log);
1394
- if (useMulti(e.dropShadow)) info.dropShadowMulti = e.dropShadow.map(i => serializeEffectObject(i, 'dropShadow', log));
1395
- if (useSingle(e.innerShadow)) info.IrSh = serializeEffectObject(e.innerShadow[0], 'innerShadow', log);
1396
- if (useMulti(e.innerShadow)) info.innerShadowMulti = e.innerShadow.map(i => serializeEffectObject(i, 'innerShadow', log));
1397
- if (e.outerGlow) info.OrGl = serializeEffectObject(e.outerGlow, 'outerGlow', log);
1398
- if (useMulti(e.solidFill)) info.solidFillMulti = e.solidFill.map(i => serializeEffectObject(i, 'solidFill', log));
1399
- if (useMulti(e.gradientOverlay)) info.gradientFillMulti = e.gradientOverlay.map(i => serializeEffectObject(i, 'gradientOverlay', log));
1382
+ if (useSingle(e.dropShadow)) info.DrSh = serializeEffectObject(e.dropShadow[0], 'dropShadow', writer);
1383
+ if (useMulti(e.dropShadow)) info.dropShadowMulti = e.dropShadow.map(i => serializeEffectObject(i, 'dropShadow', writer));
1384
+ if (useSingle(e.innerShadow)) info.IrSh = serializeEffectObject(e.innerShadow[0], 'innerShadow', writer);
1385
+ if (useMulti(e.innerShadow)) info.innerShadowMulti = e.innerShadow.map(i => serializeEffectObject(i, 'innerShadow', writer));
1386
+ if (e.outerGlow) info.OrGl = serializeEffectObject(e.outerGlow, 'outerGlow', writer);
1387
+ if (useMulti(e.solidFill)) info.solidFillMulti = e.solidFill.map(i => serializeEffectObject(i, 'solidFill', writer));
1388
+ if (useMulti(e.gradientOverlay)) info.gradientFillMulti = e.gradientOverlay.map(i => serializeEffectObject(i, 'gradientOverlay', writer));
1400
1389
  if (useMulti(e.stroke)) info.frameFXMulti = e.stroke.map(i => serializeFxObject(i));
1401
- if (e.innerGlow) info.IrGl = serializeEffectObject(e.innerGlow, 'innerGlow', log);
1402
- if (e.bevel) info.ebbl = serializeEffectObject(e.bevel, 'bevel', log);
1403
- if (useSingle(e.solidFill)) info.SoFi = serializeEffectObject(e.solidFill[0], 'solidFill', log);
1404
- if (e.patternOverlay) info.patternFill = serializeEffectObject(e.patternOverlay, 'patternOverlay', log);
1405
- if (useSingle(e.gradientOverlay)) info.GrFl = serializeEffectObject(e.gradientOverlay[0], 'gradientOverlay', log);
1406
- if (e.satin) info.ChFX = serializeEffectObject(e.satin, 'satin', log);
1390
+ if (e.innerGlow) info.IrGl = serializeEffectObject(e.innerGlow, 'innerGlow', writer);
1391
+ if (e.bevel) info.ebbl = serializeEffectObject(e.bevel, 'bevel', writer);
1392
+ if (useSingle(e.solidFill)) info.SoFi = serializeEffectObject(e.solidFill[0], 'solidFill', writer);
1393
+ if (e.patternOverlay) info.patternFill = serializeEffectObject(e.patternOverlay, 'patternOverlay', writer);
1394
+ if (useSingle(e.gradientOverlay)) info.GrFl = serializeEffectObject(e.gradientOverlay[0], 'gradientOverlay', writer);
1395
+ if (e.satin) info.ChFX = serializeEffectObject(e.satin, 'satin', writer);
1407
1396
  if (useSingle(e.stroke)) info.FrFX = serializeFxObject(e.stroke?.[0]);
1408
1397
 
1409
1398
  if (multi) {
@@ -1426,31 +1415,31 @@ export function serializeEffects(e: LayerEffectsInfo, log: boolean, multi: boole
1426
1415
  return info;
1427
1416
  }
1428
1417
 
1429
- export function parseEffects(info: Lfx2Descriptor & LmfxDescriptor, log: boolean) {
1418
+ export function parseEffects(info: Lfx2Descriptor & LmfxDescriptor, reader: PsdReader) {
1430
1419
  const effects: LayerEffectsInfo = {};
1431
- const { masterFXSwitch, DrSh, dropShadowMulti, IrSh, innerShadowMulti, OrGl, IrGl, ebbl, SoFi, solidFillMulti, patternFill, GrFl, gradientFillMulti, ChFX, FrFX, frameFXMulti, numModifyingFX, ...rest } = info;
1420
+ 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
1421
  if (!masterFXSwitch) effects.disabled = true;
1433
- if (info['Scl ']) effects.scale = parsePercent(info['Scl ']);
1434
- if (DrSh) effects.dropShadow = [parseEffectObject(DrSh, log)];
1435
- if (dropShadowMulti) effects.dropShadow = dropShadowMulti.map(i => parseEffectObject(i, log));
1436
- if (IrSh) effects.innerShadow = [parseEffectObject(IrSh, log)];
1437
- if (innerShadowMulti) effects.innerShadow = innerShadowMulti.map(i => parseEffectObject(i, log));
1438
- if (OrGl) effects.outerGlow = parseEffectObject(OrGl, log);
1439
- if (IrGl) effects.innerGlow = parseEffectObject(IrGl, log);
1440
- if (ebbl) effects.bevel = parseEffectObject(ebbl, log);
1441
- if (SoFi) effects.solidFill = [parseEffectObject(SoFi, log)];
1442
- if (solidFillMulti) effects.solidFill = solidFillMulti.map(i => parseEffectObject(i, log));
1443
- if (patternFill) effects.patternOverlay = parseEffectObject(patternFill, log);
1444
- if (GrFl) effects.gradientOverlay = [parseEffectObject(GrFl, log)];
1445
- if (gradientFillMulti) effects.gradientOverlay = gradientFillMulti.map(i => parseEffectObject(i, log));
1446
- if (ChFX) effects.satin = parseEffectObject(ChFX, log);
1422
+ if (scale) effects.scale = parsePercent(scale);
1423
+ if (DrSh) effects.dropShadow = [parseEffectObject(DrSh, reader)];
1424
+ if (dropShadowMulti) effects.dropShadow = dropShadowMulti.map(i => parseEffectObject(i, reader));
1425
+ if (IrSh) effects.innerShadow = [parseEffectObject(IrSh, reader)];
1426
+ if (innerShadowMulti) effects.innerShadow = innerShadowMulti.map(i => parseEffectObject(i, reader));
1427
+ if (OrGl) effects.outerGlow = parseEffectObject(OrGl, reader);
1428
+ if (IrGl) effects.innerGlow = parseEffectObject(IrGl, reader);
1429
+ if (ebbl) effects.bevel = parseEffectObject(ebbl, reader, 'bevel');
1430
+ if (SoFi) effects.solidFill = [parseEffectObject(SoFi, reader)];
1431
+ if (solidFillMulti) effects.solidFill = solidFillMulti.map(i => parseEffectObject(i, reader));
1432
+ if (patternFill) effects.patternOverlay = parseEffectObject(patternFill, reader);
1433
+ if (GrFl) effects.gradientOverlay = [parseEffectObject(GrFl, reader)];
1434
+ if (gradientFillMulti) effects.gradientOverlay = gradientFillMulti.map(i => parseEffectObject(i, reader));
1435
+ if (ChFX) effects.satin = parseEffectObject(ChFX, reader);
1447
1436
  if (FrFX) effects.stroke = [parseFxObject(FrFX)];
1448
1437
  if (frameFXMulti) effects.stroke = frameFXMulti.map(i => parseFxObject(i));
1449
- if (log && Object.keys(rest).length > 1) console.log('Unhandled effect keys:', rest);
1438
+ if (reader.logMissingFeatures && Object.keys(rest).length) reader.log('Unhandled effect keys:', rest);
1450
1439
  return effects;
1451
1440
  }
1452
1441
 
1453
- function parseKeyList(keyList: TimelineKeyDescriptor[], logMissingFeatures: boolean) {
1442
+ function parseKeyList(keyList: TimelineKeyDescriptor[], reader: PsdReader) {
1454
1443
  const keys: TimelineKey[] = [];
1455
1444
 
1456
1445
  for (let j = 0; j < keyList.length; j++) {
@@ -1474,7 +1463,7 @@ function parseKeyList(keyList: TimelineKeyDescriptor[], logMissingFeatures: bool
1474
1463
  break;
1475
1464
  case 'keyType.sheetStyle': {
1476
1465
  const key: TimelineKey = { interpolation, time, selected, type: 'style' };
1477
- if (animKey.sheetStyle.Lefx) key.style = parseEffects(animKey.sheetStyle.Lefx, logMissingFeatures);
1466
+ if (animKey.sheetStyle.Lefx) key.style = parseEffects(animKey.sheetStyle.Lefx, reader);
1478
1467
  keys.push(key);
1479
1468
  break;
1480
1469
  }
@@ -1492,7 +1481,7 @@ function parseKeyList(keyList: TimelineKeyDescriptor[], logMissingFeatures: bool
1492
1481
  return keys;
1493
1482
  }
1494
1483
 
1495
- function serializeKeyList(keys: TimelineKey[]): TimelineKeyDescriptor[] {
1484
+ function serializeKeyList(keys: TimelineKey[], writer: PsdWriter): TimelineKeyDescriptor[] {
1496
1485
  const keyList: TimelineKeyDescriptor[] = [];
1497
1486
 
1498
1487
  for (let j = 0; j < keys.length; j++) {
@@ -1513,7 +1502,7 @@ function serializeKeyList(keys: TimelineKey[]): TimelineKeyDescriptor[] {
1513
1502
  break;
1514
1503
  case 'style':
1515
1504
  animKey = { Type: 'keyType.sheetStyle', sheetStyle: { Vrsn: 1, blendOptions: {} } };
1516
- if (key.style) animKey.sheetStyle = { Vrsn: 1, Lefx: serializeEffects(key.style, false, false), blendOptions: {} };
1505
+ if (key.style) animKey.sheetStyle = { Vrsn: 1, Lefx: serializeEffects(key.style, writer, false), blendOptions: {} };
1517
1506
  break;
1518
1507
  case 'globalLighting': {
1519
1508
  animKey = { Type: 'keyType.globalLighting', gblA: key.globalAngle, globalAltitude: key.globalAltitude };
@@ -1528,7 +1517,7 @@ function serializeKeyList(keys: TimelineKey[]): TimelineKeyDescriptor[] {
1528
1517
  return keyList;
1529
1518
  }
1530
1519
 
1531
- export function parseTrackList(trackList: TimelineTrackDescriptor[], logMissingFeatures: boolean) {
1520
+ export function parseTrackList(trackList: TimelineTrackDescriptor[], reader: PsdReader) {
1532
1521
  const tracks: TimelineTrack[] = [];
1533
1522
 
1534
1523
  for (let i = 0; i < trackList.length; i++) {
@@ -1536,14 +1525,14 @@ export function parseTrackList(trackList: TimelineTrackDescriptor[], logMissingF
1536
1525
  const track: TimelineTrack = {
1537
1526
  type: stdTrackID.decode(tr.trackID),
1538
1527
  enabled: tr.enab,
1539
- keys: parseKeyList(tr.keyList, logMissingFeatures),
1528
+ keys: parseKeyList(tr.keyList, reader),
1540
1529
  };
1541
1530
 
1542
1531
  if (tr.effectParams) {
1543
1532
  track.effectParams = {
1544
1533
  fillCanvas: tr.effectParams.fillCanvas,
1545
1534
  zoomOrigin: tr.effectParams.zoomOrigin,
1546
- keys: parseKeyList(tr.effectParams.keyList, logMissingFeatures),
1535
+ keys: parseKeyList(tr.effectParams.keyList, reader),
1547
1536
  };
1548
1537
  }
1549
1538
 
@@ -1553,7 +1542,7 @@ export function parseTrackList(trackList: TimelineTrackDescriptor[], logMissingF
1553
1542
  return tracks;
1554
1543
  }
1555
1544
 
1556
- export function serializeTrackList(tracks: TimelineTrack[]): TimelineTrackDescriptor[] {
1545
+ export function serializeTrackList(tracks: TimelineTrack[], writer: PsdWriter): TimelineTrackDescriptor[] {
1557
1546
  const trackList: TimelineTrackDescriptor[] = [];
1558
1547
 
1559
1548
  for (let i = 0; i < tracks.length; i++) {
@@ -1565,12 +1554,12 @@ export function serializeTrackList(tracks: TimelineTrack[]): TimelineTrackDescri
1565
1554
  Effc: !!t.effectParams,
1566
1555
  ...(t.effectParams ? {
1567
1556
  effectParams: {
1568
- keyList: serializeKeyList(t.effectParams.keys),
1557
+ keyList: serializeKeyList(t.effectParams.keys, writer),
1569
1558
  fillCanvas: t.effectParams.fillCanvas,
1570
1559
  zoomOrigin: t.effectParams.zoomOrigin,
1571
1560
  }
1572
1561
  } : {}),
1573
- keyList: serializeKeyList(t.keys),
1562
+ keyList: serializeKeyList(t.keys, writer),
1574
1563
  });
1575
1564
  }
1576
1565
 
@@ -1581,7 +1570,7 @@ type AllEffects = LayerEffectShadow & LayerEffectsOuterGlow & LayerEffectStroke
1581
1570
  LayerEffectInnerGlow & LayerEffectBevel & LayerEffectSolidFill &
1582
1571
  LayerEffectPatternOverlay & LayerEffectSatin & LayerEffectGradientOverlay;
1583
1572
 
1584
- function parseEffectObject(obj: any, reportErrors: boolean) {
1573
+ function parseEffectObject(obj: any, reader: PsdReader, objName?: string) {
1585
1574
  const result: AllEffects = {} as any;
1586
1575
 
1587
1576
  for (const key of Object.keys(obj)) {
@@ -1608,7 +1597,10 @@ function parseEffectObject(obj: any, reportErrors: boolean) {
1608
1597
  case 'GlwT': result.technique = BETE.decode(val) as any; break;
1609
1598
  case 'glwS': result.source = IGSr.decode(val); break;
1610
1599
  case 'Type': result.type = GrdT.decode(val); break;
1611
- case 'gs99': result.interpolationMethod = gradientInterpolationMethodType.decode(val); break;
1600
+ case 'gs99': // gradient overlay
1601
+ case 'gradientsInterpolationMethod': // glows
1602
+ result.interpolationMethod = gradientInterpolationMethodType.decode(val);
1603
+ break;
1612
1604
  case 'Opct': result.opacity = parsePercent(val); break;
1613
1605
  case 'hglO': result.highlightOpacity = parsePercent(val); break;
1614
1606
  case 'sdwO': result.shadowOpacity = parsePercent(val); break;
@@ -1625,15 +1617,23 @@ function parseEffectObject(obj: any, reportErrors: boolean) {
1625
1617
  case 'Dstn': result.distance = parseUnits(val); break;
1626
1618
  case 'Scl ': result.scale = parsePercent(val); break;
1627
1619
  case 'Ptrn': result.pattern = { name: val['Nm '], id: val.Idnt }; break;
1628
- case 'phase': result.phase = { x: val.Hrzn, y: val.Vrtc }; break;
1620
+ case 'phase': result.phase = parsePhase(val); break;
1629
1621
  case 'Ofst': result.offset = { x: parsePercent(val.Hrzn), y: parsePercent(val.Vrtc) }; break;
1630
1622
  case 'MpgS':
1631
- case 'TrnS':
1632
- result.contour = {
1623
+ case 'TrnS': {
1624
+ const contour: EffectContour = {
1633
1625
  name: val['Nm '],
1634
1626
  curve: (val['Crv '] as any[]).map(p => ({ x: p.Hrzn, y: p.Vrtc })),
1635
1627
  };
1628
+
1629
+ // bevel can have both: gloss contour (TrnS) and contour of the shape (MpgS)
1630
+ if (objName === 'bevel' && key === 'MpgS') {
1631
+ result.shapeContour = contour;
1632
+ } else {
1633
+ result.contour = contour;
1634
+ }
1636
1635
  break;
1636
+ }
1637
1637
  case 'Grad': result.gradient = parseGradient(val); break;
1638
1638
  case 'useTexture':
1639
1639
  case 'useShape':
@@ -1645,14 +1645,14 @@ function parseEffectObject(obj: any, reportErrors: boolean) {
1645
1645
  case '_classID':
1646
1646
  break;
1647
1647
  default:
1648
- reportErrors && console.log(`Invalid effect key: '${key}', value:`, val);
1648
+ reader.logMissingFeatures && reader.log(`Invalid effect key: '${key}', value:`, val);
1649
1649
  }
1650
1650
  }
1651
1651
 
1652
1652
  return result;
1653
1653
  }
1654
1654
 
1655
- function serializeEffectObject(obj: any, objName: string, reportErrors: boolean) {
1655
+ function serializeEffectObject(obj: any, objName: string, writer: PsdWriter) {
1656
1656
  const result: any = {
1657
1657
  enab: false,
1658
1658
  };
@@ -1691,7 +1691,9 @@ function serializeEffectObject(obj: any, objName: string, reportErrors: boolean)
1691
1691
  break;
1692
1692
  case 'source': result.glwS = IGSr.encode(val); break;
1693
1693
  case 'type': result.Type = GrdT.encode(val); break;
1694
- case 'interpolationMethod': result.gs99 = gradientInterpolationMethodType.encode(val); break;
1694
+ case 'interpolationMethod':
1695
+ result[(objName === 'outerGlow' || objName === 'innerGlow') ? 'gradientsInterpolationMethod' : 'gs99'] = gradientInterpolationMethodType.encode(val);
1696
+ break;
1695
1697
  case 'opacity': result.Opct = unitsPercent(val); break;
1696
1698
  case 'highlightOpacity': result.hglO = unitsPercent(val); break;
1697
1699
  case 'shadowOpacity': result.sdwO = unitsPercent(val); break;
@@ -1714,14 +1716,9 @@ function serializeEffectObject(obj: any, objName: string, reportErrors: boolean)
1714
1716
  case 'scale': result['Scl '] = unitsPercent(val); break;
1715
1717
  case 'pattern': result.Ptrn = { 'Nm ': val.name, Idnt: val.id }; break;
1716
1718
  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
- }
1719
+ case 'offset': result.Ofst = { Hrzn: unitsPercentF(val.x), Vrtc: unitsPercentF(val.y) }; break;
1720
+ case 'contour': result[objName === 'satin' ? 'MpgS' : 'TrnS'] = serializeContour(val); break;
1721
+ case 'shapeContour': result.MpgS = serializeContour(val); break;
1725
1722
  case 'gradient': result.Grad = serializeGradient(val); break;
1726
1723
  case 'useTexture':
1727
1724
  case 'useShape':
@@ -1732,13 +1729,20 @@ function serializeEffectObject(obj: any, objName: string, reportErrors: boolean)
1732
1729
  result[key] = val;
1733
1730
  break;
1734
1731
  default:
1735
- reportErrors && console.log(`Invalid effect key: '${key}', value:`, val);
1732
+ writer.logMissingFeatures && writer.log(`Invalid effect key: '${key}', value:`, val);
1736
1733
  }
1737
1734
  }
1738
1735
 
1739
1736
  return result;
1740
1737
  }
1741
1738
 
1739
+ function serializeContour(contour: EffectContour) {
1740
+ return {
1741
+ 'Nm ': contour.name,
1742
+ 'Crv ': contour.curve.map(p => ({ Hrzn: p.x, Vrtc: p.y })),
1743
+ };
1744
+ }
1745
+
1742
1746
  function parseGradient(grad: DesciptorGradient): EffectSolidGradient | EffectNoiseGradient {
1743
1747
  if (grad.GrdF === 'GrdF.CstS') {
1744
1748
  // stop locations are always in 0..4096 range, independent of smoothness (Intr)
@@ -1748,11 +1752,22 @@ function parseGradient(grad: DesciptorGradient): EffectSolidGradient | EffectNoi
1748
1752
  type: 'solid',
1749
1753
  name: grad['Nm '],
1750
1754
  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
- })),
1755
+ colorStops: grad.Clrs.map(s => {
1756
+ const stop: ColorStop = {
1757
+ color: s['Clr '] ? parseColor(s['Clr ']) : { r: 0, g: 0, b: 0 },
1758
+ location: s.Lctn / samples,
1759
+ midpoint: s.Mdpn / 100,
1760
+ };
1761
+
1762
+ if (s.Type) {
1763
+ try {
1764
+ const type = Clry.decode(s.Type);
1765
+ if (type !== 'user') stop.type = type;
1766
+ } catch { } // unknown stop type, read as user color
1767
+ }
1768
+
1769
+ return stop;
1770
+ }),
1756
1771
  opacityStops: grad.Trns.map(s => ({
1757
1772
  opacity: parsePercent(s.Opct),
1758
1773
  location: s.Lctn / samples,
@@ -1784,12 +1799,12 @@ function serializeGradient(grad: EffectSolidGradient | EffectNoiseGradient): Des
1784
1799
  Intr: Math.round((grad.smoothness ?? 1) * 4096),
1785
1800
  Clrs: grad.colorStops.map(s => ({
1786
1801
  'Clr ': serializeColor(s.color),
1787
- Type: 'Clry.UsrS',
1802
+ Type: Clry.encode(s.type ?? 'user'),
1788
1803
  Lctn: Math.round(s.location * samples),
1789
1804
  Mdpn: Math.round((s.midpoint ?? 0.5) * 100),
1790
1805
  })),
1791
1806
  Trns: grad.opacityStops.map(s => ({
1792
- Opct: unitsPercent(s.opacity),
1807
+ Opct: unitsPercentF(s.opacity),
1793
1808
  Lctn: Math.round(s.location * samples),
1794
1809
  Mdpn: Math.round((s.midpoint ?? 0.5) * 100),
1795
1810
  })),
@@ -1827,13 +1842,23 @@ function parseGradientContent(descriptor: DescriptorGradientContent) {
1827
1842
  return result;
1828
1843
  }
1829
1844
 
1845
+ // phase is usually stored as plain numbers, but some files store it as unit values
1846
+ function parsePhase({ Hrzn, Vrtc }: { Hrzn: number | DescriptorUnitsValue; Vrtc: number | DescriptorUnitsValue; }) {
1847
+ return {
1848
+ x: typeof Hrzn === 'number' ? Hrzn : Hrzn.value,
1849
+ y: typeof Vrtc === 'number' ? Vrtc : Vrtc.value,
1850
+ };
1851
+ }
1852
+
1830
1853
  function parsePatternContent(descriptor: DescriptorPatternContent) {
1831
1854
  const result: EffectPattern & ExtraPatternInfo = {
1832
1855
  name: descriptor.Ptrn['Nm '],
1833
1856
  id: descriptor.Ptrn.Idnt,
1834
1857
  };
1835
1858
  if (descriptor.Lnkd !== undefined) result.linked = descriptor.Lnkd;
1836
- if (descriptor.phase !== undefined) result.phase = { x: descriptor.phase.Hrzn, y: descriptor.phase.Vrtc };
1859
+ if (descriptor.phase !== undefined) result.phase = parsePhase(descriptor.phase);
1860
+ if (descriptor['Scl '] !== undefined) result.scale = parsePercent(descriptor['Scl ']);
1861
+ if (descriptor.Angl !== undefined) result.angle = parseAngle(descriptor.Angl);
1837
1862
  return result;
1838
1863
  }
1839
1864
 
@@ -1858,11 +1883,11 @@ function serializeGradientContent(content: (EffectSolidGradient | EffectNoiseGra
1858
1883
  if (content.angle !== undefined) result.Angl = unitsAngle(content.angle);
1859
1884
  result.Type = GrdT.encode(content.style);
1860
1885
  if (content.align !== undefined) result.Algn = content.align;
1861
- if (content.scale !== undefined) result['Scl '] = unitsPercent(content.scale);
1886
+ if (content.scale !== undefined) result['Scl '] = unitsPercentF(content.scale);
1862
1887
  if (content.offset) {
1863
1888
  result.Ofst = {
1864
- Hrzn: unitsPercent(content.offset.x),
1865
- Vrtc: unitsPercent(content.offset.y),
1889
+ Hrzn: unitsPercentF(content.offset.x),
1890
+ Vrtc: unitsPercentF(content.offset.y),
1866
1891
  };
1867
1892
  }
1868
1893
  result.Grad = serializeGradient(content);
@@ -1878,6 +1903,8 @@ function serializePatternContent(content: EffectPattern & ExtraPatternInfo) {
1878
1903
  };
1879
1904
  if (content.linked !== undefined) result.Lnkd = !!content.linked;
1880
1905
  if (content.phase !== undefined) result.phase = { Hrzn: content.phase.x, Vrtc: content.phase.y };
1906
+ if (content.scale !== undefined) result['Scl '] = unitsPercentF(content.scale);
1907
+ if (content.angle !== undefined) result.Angl = unitsAngle(content.angle);
1881
1908
  return result;
1882
1909
  }
1883
1910
 
@@ -1894,10 +1921,9 @@ export function serializeVectorContent(content: VectorContent): { descriptor: De
1894
1921
  // Descriptor color values: HSB saturation/brightness 0..100, CMYK 0..100, Gray 0..100, Lab L 0..100, a/b -128..127
1895
1922
  // 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
1923
 
1897
- const colorSourceKey = typeof Symbol !== 'undefined' ? Symbol('agPsdColorSource') : '__agPsdColorSource';
1898
-
1899
1924
  const colorConversions: { [key: string]: ((x: number) => number)[] | undefined; } = {
1900
1925
  // key: [descriptor -> color, color -> descriptor]
1926
+ 'H ': [x => x / 360, x => x * 360], // angle
1901
1927
  Strt: [x => x / 100, x => x * 100],
1902
1928
  Brgh: [x => x / 100, x => x * 100],
1903
1929
  'Cyn ': [x => x * 255 / 100, x => x * 100 / 255],
@@ -1926,38 +1952,63 @@ function colorToDescriptorValue(key: string, value: number) {
1926
1952
  return result;
1927
1953
  }
1928
1954
 
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]);
1955
+ function isBookColor(color: any) {
1956
+ return color._classID === 'BkCl' || 'Bk ' in color || 'bookID' in color;
1933
1957
  }
1934
1958
 
1935
1959
  export function parseColor(color: DescriptorColor): Color {
1936
- let result: Color;
1960
+ const result = parseColorValues(color);
1961
+
1962
+ if (isBookColor(color)) {
1963
+ const book = color as any as { 'Bk '?: string; 'Nm '?: string; bookID?: number; bookKey?: Uint8Array | string; };
1964
+ const key = book.bookKey;
1965
+ result.book = {
1966
+ book: book['Bk '] ?? '',
1967
+ name: book['Nm '] ?? '',
1968
+ bookID: book.bookID ?? 0,
1969
+ bookKey: typeof key === 'string' ? key : (key ? String.fromCharCode(...Array.from(key)) : ''),
1970
+ };
1971
+ }
1972
+
1973
+ return result;
1974
+ }
1937
1975
 
1976
+ function parseColorValues(color: DescriptorColor): Color {
1938
1977
  if ('H ' in color) {
1939
- result = { h: parsePercentOrAngle(color['H ']), s: color.Strt / 100, b: color.Brgh / 100 };
1978
+ return { h: parsePercentOrAngle(color['H ']), s: color.Strt / 100, b: color.Brgh / 100 };
1940
1979
  } else if ('Rd ' in color) {
1941
1980
  return { r: color['Rd '], g: color['Grn '], b: color['Bl '] };
1942
1981
  } else if ('Cyn ' in color) {
1943
- result = {
1982
+ return {
1944
1983
  c: color['Cyn '] * 255 / 100,
1945
1984
  m: color.Mgnt * 255 / 100,
1946
1985
  y: color['Ylw '] * 255 / 100,
1947
1986
  k: color.Blck * 255 / 100,
1948
1987
  };
1949
1988
  } else if ('Gry ' in color) {
1950
- result = { k: color['Gry '] * 255 / 100 };
1989
+ return { k: color['Gry '] * 255 / 100 };
1951
1990
  } else if ('Lmnc' in color) {
1952
- result = { l: color.Lmnc / 100, a: normalizeLabAB(color['A ']), b: normalizeLabAB(color['B ']) };
1991
+ return { l: color.Lmnc / 100, a: normalizeLabAB(color['A ']), b: normalizeLabAB(color['B ']) };
1953
1992
  } else if ('redFloat' in color) {
1954
1993
  return { fr: color.redFloat, fg: color.greenFloat, fb: color.blueFloat };
1994
+ } else if (isBookColor(color)) {
1995
+ return { r: 0, g: 0, b: 0 }; // book (Pantone) color without color values
1955
1996
  } else {
1956
1997
  throw new Error('Unsupported color descriptor');
1957
1998
  }
1999
+ }
2000
+
2001
+ export function serializeColor(color: Color | undefined): DescriptorColor {
2002
+ const result = serializeColorValues(color);
2003
+
2004
+ if (color?.book) {
2005
+ const { book, name, bookID, bookKey } = color.book;
2006
+ const key = new Uint8Array(bookKey.length);
2007
+ for (let i = 0; i < key.length; i++) key[i] = bookKey.charCodeAt(i) & 0xff;
2008
+ Object.assign(result, { 'Bk ': book, 'Nm ': name, bookID, bookKey: key });
2009
+ }
1958
2010
 
1959
- // remember original descriptor values, so unmodified colors can be written back without conversion errors
1960
- return setHidden(result, colorSourceKey, { color: { ...result }, descriptor: color });
2011
+ return result;
1961
2012
  }
1962
2013
 
1963
2014
  function serializeColorValues(color: Color | undefined): DescriptorColor {
@@ -1970,7 +2021,7 @@ function serializeColorValues(color: Color | undefined): DescriptorColor {
1970
2021
  } else if ('h' in color) {
1971
2022
  return {
1972
2023
  _name: '', _classID: 'HSBC',
1973
- 'H ': unitsAngle((color.h || 0) * 360),
2024
+ 'H ': unitsAngle(colorToDescriptorValue('H ', color.h || 0)),
1974
2025
  Strt: colorToDescriptorValue('Strt', color.s || 0),
1975
2026
  Brgh: colorToDescriptorValue('Brgh', color.b || 0),
1976
2027
  };
@@ -1996,22 +2047,6 @@ function serializeColorValues(color: Color | undefined): DescriptorColor {
1996
2047
  }
1997
2048
  }
1998
2049
 
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
2050
  export function parseAngle(x: DescriptorUnitsValue) {
2016
2051
  if (x === undefined) return 0;
2017
2052
  if (x.units !== 'Angle') throw new Error(`Invalid units: ${x.units}`);
@@ -2034,7 +2069,8 @@ export function parsePercentOrAngle(x: DescriptorUnitsValue | undefined) {
2034
2069
  export function parseUnits({ units, value }: DescriptorUnitsValue): UnitsValue {
2035
2070
  if (
2036
2071
  units !== 'Pixels' && units !== 'Millimeters' && units !== 'Points' && units !== 'None' &&
2037
- units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density'
2072
+ units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density' &&
2073
+ units !== 'Distance'
2038
2074
  ) {
2039
2075
  throw new Error(`Invalid units: ${JSON.stringify({ units, value })}`);
2040
2076
  }
@@ -2076,7 +2112,8 @@ export function unitsValue(x: UnitsValue | undefined, key: string): DescriptorUn
2076
2112
 
2077
2113
  if (
2078
2114
  units !== 'Pixels' && units !== 'Millimeters' && units !== 'Points' && units !== 'None' &&
2079
- units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density'
2115
+ units !== 'Picas' && units !== 'Inches' && units !== 'Centimeters' && units !== 'Density' &&
2116
+ units !== 'Distance'
2080
2117
  ) {
2081
2118
  throw new Error(`Invalid units in ${JSON.stringify(x)} (key: ${key})`);
2082
2119
  }
@@ -2222,6 +2259,12 @@ export const gradientInterpolationMethodType = createEnum<InterpolationMethod>('
2222
2259
  // TODO: stripes
2223
2260
  });
2224
2261
 
2262
+ export const Clry = createEnum<'user' | 'foreground' | 'background'>('Clry', 'user', {
2263
+ user: 'UsrS',
2264
+ foreground: 'FrgC',
2265
+ background: 'BckC',
2266
+ });
2267
+
2225
2268
  export const ClrS = createEnum<'rgb' | 'hsb' | 'lab' | 'hsl'>('ClrS', 'rgb', {
2226
2269
  rgb: 'RGBC',
2227
2270
  hsb: 'HSBl',
@@ -2451,3 +2494,9 @@ export const presetKindType = createEnum<'custom' | 'default'>('presetKindType',
2451
2494
  custom: 'presetKindCustom',
2452
2495
  default: 'presetKindDefault',
2453
2496
  });
2497
+
2498
+ export const patternColorHandling = createEnum<'convert' | 'avoid conversion' | 'luminance only'>('', 'convert', {
2499
+ 'convert': 'conv',
2500
+ 'avoid conversion': 'avod',
2501
+ 'luminance only': 'lumi',
2502
+ });