ag-psd 32.0.0 → 34.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/cff9434a +0 -0
  18. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  19. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  20. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  21. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6745c2b +0 -0
  22. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  23. package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
  24. package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
  25. package/CHANGELOG.md +100 -0
  26. package/LICENSE +5 -0
  27. package/README.md +4 -7
  28. package/README_PSD.md +261 -84
  29. package/TODO.md +13 -84
  30. package/dist/abr.d.ts +11 -4
  31. package/dist/abr.js +362 -230
  32. package/dist/abr.js.map +1 -1
  33. package/dist/additionalInfo.d.ts +56 -2
  34. package/dist/additionalInfo.js +828 -361
  35. package/dist/additionalInfo.js.map +1 -1
  36. package/dist/ase.d.ts +6 -1
  37. package/dist/ase.js +61 -61
  38. package/dist/ase.js.map +1 -1
  39. package/dist/bundle.js +3321 -1364
  40. package/dist/descriptor.d.ts +28 -13
  41. package/dist/descriptor.js +295 -241
  42. package/dist/descriptor.js.map +1 -1
  43. package/dist/helpers.d.ts +7 -1
  44. package/dist/helpers.js +246 -4
  45. package/dist/helpers.js.map +1 -1
  46. package/dist/imageResources.d.ts +15 -3
  47. package/dist/imageResources.js +586 -144
  48. package/dist/imageResources.js.map +1 -1
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/jpeg.js +8 -8
  53. package/dist/jpeg.js.map +1 -1
  54. package/dist/psd.d.ts +292 -19
  55. package/dist/psd.js.map +1 -1
  56. package/dist/psdReader.d.ts +10 -0
  57. package/dist/psdReader.js +424 -216
  58. package/dist/psdReader.js.map +1 -1
  59. package/dist/psdWriter.d.ts +2 -0
  60. package/dist/psdWriter.js +373 -113
  61. package/dist/psdWriter.js.map +1 -1
  62. package/dist/text.d.ts +32 -1
  63. package/dist/text.js +75 -6
  64. package/dist/text.js.map +1 -1
  65. package/dist/utf8.js +0 -3
  66. package/dist/utf8.js.map +1 -1
  67. package/dist-es/abr.d.ts +11 -4
  68. package/dist-es/abr.js +362 -230
  69. package/dist-es/abr.js.map +1 -1
  70. package/dist-es/additionalInfo.d.ts +56 -2
  71. package/dist-es/additionalInfo.js +828 -361
  72. package/dist-es/additionalInfo.js.map +1 -1
  73. package/dist-es/ase.d.ts +6 -1
  74. package/dist-es/ase.js +61 -61
  75. package/dist-es/ase.js.map +1 -1
  76. package/dist-es/descriptor.d.ts +28 -13
  77. package/dist-es/descriptor.js +295 -241
  78. package/dist-es/descriptor.js.map +1 -1
  79. package/dist-es/helpers.d.ts +7 -1
  80. package/dist-es/helpers.js +246 -4
  81. package/dist-es/helpers.js.map +1 -1
  82. package/dist-es/imageResources.d.ts +15 -3
  83. package/dist-es/imageResources.js +586 -144
  84. package/dist-es/imageResources.js.map +1 -1
  85. package/dist-es/index.d.ts +1 -0
  86. package/dist-es/index.js +1 -0
  87. package/dist-es/index.js.map +1 -1
  88. package/dist-es/jpeg.js +8 -8
  89. package/dist-es/jpeg.js.map +1 -1
  90. package/dist-es/psd.d.ts +292 -19
  91. package/dist-es/psd.js.map +1 -1
  92. package/dist-es/psdReader.d.ts +10 -0
  93. package/dist-es/psdReader.js +424 -216
  94. package/dist-es/psdReader.js.map +1 -1
  95. package/dist-es/psdWriter.d.ts +2 -0
  96. package/dist-es/psdWriter.js +373 -113
  97. package/dist-es/psdWriter.js.map +1 -1
  98. package/dist-es/text.d.ts +32 -1
  99. package/dist-es/text.js +75 -6
  100. package/dist-es/text.js.map +1 -1
  101. package/dist-es/utf8.js +0 -3
  102. package/dist-es/utf8.js.map +1 -1
  103. package/package.json +2 -2
  104. package/perf.js +312 -9
  105. package/src/abr.ts +382 -235
  106. package/src/additionalInfo.ts +1071 -482
  107. package/src/ase.ts +66 -61
  108. package/src/descriptor.ts +291 -242
  109. package/src/helpers.ts +266 -3
  110. package/src/imageResources.ts +736 -169
  111. package/src/index.ts +1 -0
  112. package/src/jpeg.ts +8 -8
  113. package/src/psd.ts +271 -32
  114. package/src/psdReader.ts +440 -221
  115. package/src/psdWriter.ts +402 -110
  116. package/src/text.ts +93 -7
  117. package/src/utf8.ts +0 -4
@@ -1,21 +1,36 @@
1
1
  import { toByteArray } from 'base64-js';
2
- import { BlendMode, ImageResources, RenderingIntent } from './psd';
3
- import { PsdReader, readUnicodeString, readUint32, readUint16, readUint8, readFloat64, readBytes, skipBytes, readFloat32, readInt16, readFixedPoint32, readSignature, checkSignature, readSection, readColor, readInt32, readAsciiString } from './psdReader';
4
- import { PsdWriter, writeUnicodeString, writeUint32, writeUint8, writeFloat64, writeUint16, writeBytes, writeInt16, writeFloat32, writeFixedPoint32, writeUnicodeStringWithPadding, writeColor, writeSignature, writeSection, writeInt32, writeAsciiString, } from './psdWriter';
5
- import { createCanvasFromData, createEnum, MOCK_HANDLERS } from './helpers';
6
- import { decodeString, encodeString } from './utf8';
7
- import { ESliceBGColorType, ESliceHorzAlign, ESliceOrigin, ESliceType, ESliceVertAlign, frac, FractionDescriptor, parseTrackList, pathSelectionStateToReference, readVersionAndDescriptor, serializeTrackList, TimelineTrackDescriptor, TimeScopeDescriptor, writeVersionAndDescriptor } from './descriptor';
2
+ import { BlendMode, Color, HalftoneScreen, ImageResources, KeyDescriptorItem, PrintTransferFunction, RenderingIntent, SavedPath } from './psd';
3
+ import { KeyDescriptorListDescriptor, parseKeyDescriptorList, readVectorMask, serializeKeyDescriptorList, writeVectorMask } from './additionalInfo';
4
+ import { PsdReader, readUnicodeString, readUint32, readUint16, readUint8, readFloat64, readBytes, skipBytes, readFloat32, readInt16, readFixedPoint32, readSignature, checkSignature, readSection, readColor, readInt32, readAsciiString, decodeUtf8, getUtf8StrictDecoder, getGbkStrictDecoder } from './psdReader';
5
+ import { PsdWriter, writeUnicodeString, writeUint32, writeUint8, writeFloat64, writeUint16, writeBytes, writeInt16, writeFloat32, writeFixedPoint32, writeUnicodeStringWithPadding, writeColor, writeSignature, writeSection, writeInt32, writeAsciiString, writeZeros } from './psdWriter';
6
+ import { clamp, ColorSpace, createCanvasFromData, createEnum, createImageData, imageDataToCanvas, MOCK_HANDLERS } from './helpers';
7
+ import { encodeString } from './utf8';
8
+ import { ESliceBGColorType, ESliceHorzAlign, ESliceOrigin, ESliceType, ESliceVertAlign, frac, FractionDescriptor, parseTrackList, pathSelectionStateToReference, DescriptorReferenceItem, readVersionAndDescriptor, serializeTrackList, TimelineTrackDescriptor, TimeScopeDescriptor, writeVersionAndDescriptor, DescriptorColor, DescriptorUnitsValue, parseColor, serializeColor, parseUnits, unitsValue, parsePercent, unitsPercentF , outputSettingsRoot } from './descriptor';
8
9
 
9
10
  export interface InternalImageResources extends ImageResources {
10
11
  layersGroup?: number[];
11
12
  layerGroupsEnabledId?: number[];
13
+ savedPathsOrigination?: KeyDescriptorItem[][];
14
+ }
15
+
16
+ export interface ResourceDocumentInfo {
17
+ width: number;
18
+ height: number;
19
+ }
20
+
21
+ export interface ResourceInfo extends ResourceDocumentInfo {
22
+ signature: string; // '8BIM', or 'MeSa' etc. for resources from other applications
23
+ id: number;
24
+ name: Uint8Array; // raw pascal string bytes
12
25
  }
13
26
 
14
27
  export interface ResourceHandler {
15
28
  key: number;
29
+ keyCount: number; // number of consecutive resource ids handled, starting at `key` (index is added to `key` when writing)
16
30
  has: (target: InternalImageResources) => boolean | number;
17
- read: (reader: PsdReader, target: InternalImageResources, left: () => number) => void;
18
- write: (writer: PsdWriter, target: InternalImageResources, index: number) => void;
31
+ read: (reader: PsdReader, target: InternalImageResources, left: () => number, info: ResourceInfo) => void;
32
+ write: (writer: PsdWriter, target: InternalImageResources, index: number, document: ResourceDocumentInfo) => void;
33
+ name?: (target: InternalImageResources, index: number) => string;
19
34
  }
20
35
 
21
36
  export const resourceHandlers: ResourceHandler[] = [];
@@ -24,12 +39,13 @@ export const resourceHandlersMap: { [key: number]: ResourceHandler } = {};
24
39
  function addHandler(
25
40
  key: number,
26
41
  has: (target: InternalImageResources) => boolean | number,
27
- read: (reader: PsdReader, target: InternalImageResources, left: () => number) => void,
28
- write: (writer: PsdWriter, target: InternalImageResources, index: number) => void,
42
+ read: (reader: PsdReader, target: InternalImageResources, left: () => number, info: ResourceInfo) => void,
43
+ write: (writer: PsdWriter, target: InternalImageResources, index: number, document: ResourceDocumentInfo) => void,
44
+ options: { keyCount?: number; name?: (target: InternalImageResources, index: number) => string; } = {},
29
45
  ) {
30
- const handler: ResourceHandler = { key, has, read, write };
46
+ const handler: ResourceHandler = { key, keyCount: options.keyCount ?? 1, has, read, write, name: options.name };
31
47
  resourceHandlers.push(handler);
32
- resourceHandlersMap[handler.key] = handler;
48
+ for (let i = 0; i < handler.keyCount; i++) resourceHandlersMap[handler.key + i] = handler;
33
49
  }
34
50
 
35
51
  const LOG_MOCK_HANDLERS = false;
@@ -49,7 +65,7 @@ function byteAt(value: string, index: number) {
49
65
 
50
66
  function readUtf8String(reader: PsdReader, length: number) {
51
67
  const buffer = readBytes(reader, length);
52
- return decodeString(buffer);
68
+ return decodeUtf8(reader, buffer);
53
69
  }
54
70
 
55
71
  function writeUtf8String(writer: PsdWriter, value: string) {
@@ -57,24 +73,13 @@ function writeUtf8String(writer: PsdWriter, value: string) {
57
73
  writeBytes(writer, buffer);
58
74
  }
59
75
 
60
- let gbkDecoder: TextDecoder | null | undefined;
61
-
62
- function getGbkDecoder() {
63
- if (gbkDecoder === undefined) {
64
- try {
65
- gbkDecoder = new TextDecoder('gbk');
66
- } catch {
67
- gbkDecoder = null;
68
- }
69
- }
70
-
71
- return gbkDecoder;
72
- }
73
-
74
76
  function readEncodedString(reader: PsdReader) {
75
77
  const length = readUint8(reader);
76
78
  const buffer = readBytes(reader, length);
79
+ return decodeEncodedString(reader, buffer);
80
+ }
77
81
 
82
+ function decodeEncodedString(reader: PsdReader, buffer: Uint8Array) {
78
83
  let notAscii = false;
79
84
  for (let i = 0; i < buffer.byteLength; i++) {
80
85
  if (buffer[i] & 0x80) {
@@ -84,19 +89,21 @@ function readEncodedString(reader: PsdReader) {
84
89
  }
85
90
 
86
91
  if (notAscii) {
87
- const decoder = getGbkDecoder();
88
-
89
- if (decoder) {
90
- try {
91
- return decoder.decode(buffer);
92
- } catch { }
92
+ // encoding is ambiguous, many GBK strings are also valid UTF-8 and vice versa, GBK is preferred,
93
+ // UTF-8 is only used if the string is not valid GBK
94
+ for (const decoder of [getGbkStrictDecoder(reader), getUtf8StrictDecoder(reader)]) {
95
+ if (decoder) {
96
+ try {
97
+ return decoder.decode(buffer);
98
+ } catch { }
99
+ }
93
100
  }
94
101
  }
95
102
 
96
- return decodeString(buffer);
103
+ return decodeUtf8(reader, buffer);
97
104
  }
98
105
 
99
- function writeEncodedString(writer: PsdWriter, value: string) {
106
+ function toShortAsciiString(value: string) {
100
107
  let ascii = '';
101
108
 
102
109
  for (let i = 0; i < value.length;) {
@@ -106,22 +113,23 @@ function writeEncodedString(writer: PsdWriter, value: string) {
106
113
  }
107
114
 
108
115
  // length is stored in a single byte
109
- if (ascii.length > 255) ascii = ascii.substring(0, 255);
116
+ return ascii.length > 255 ? ascii.substring(0, 255) : ascii;
117
+ }
110
118
 
111
- const buffer = encodeString(ascii);
119
+ function writeEncodedString(writer: PsdWriter, value: string) {
120
+ const buffer = encodeString(toShortAsciiString(value));
112
121
  writeUint8(writer, buffer.byteLength);
113
122
  writeBytes(writer, buffer);
114
123
  }
115
124
 
116
- MOCK_HANDLERS && addHandler(
125
+ addHandler(
117
126
  1028, // IPTC-NAA record
118
- target => (target as any)._ir1028 !== undefined,
127
+ target => target.iptcData !== undefined,
119
128
  (reader, target, left) => {
120
- LOG_MOCK_HANDLERS && console.log('image resource 1028', left());
121
- (target as any)._ir1028 = readBytes(reader, left());
129
+ target.iptcData = readBytes(reader, left());
122
130
  },
123
131
  (writer, target) => {
124
- writeBytes(writer, (target as any)._ir1028);
132
+ writeBytes(writer, target.iptcData!);
125
133
  },
126
134
  );
127
135
 
@@ -183,14 +191,14 @@ interface PrintInformationDescriptor {
183
191
  };
184
192
  }
185
193
 
186
- MOCK_HANDLERS && addHandler(
194
+ addHandler(
187
195
  1085, // Windows DEVMODE. Variable OS specific info for Windows.
188
- target => (target as any)._ir1085 !== undefined,
196
+ target => target.windowsDevMode !== undefined,
189
197
  (reader, target, left) => {
190
- (target as any)._ir1085 = readBytes(reader, left());
198
+ target.windowsDevMode = readBytes(reader, left());
191
199
  },
192
200
  (writer, target) => {
193
- writeBytes(writer, (target as any)._ir1085);
201
+ writeBytes(writer, target.windowsDevMode!);
194
202
  }
195
203
  );
196
204
 
@@ -208,6 +216,7 @@ addHandler(
208
216
  const info = target.printInformation;
209
217
 
210
218
  if (desc.PstS !== undefined) info.printerManagesColors = desc.PstS;
219
+ if (desc.ClrS === 'ClrS.Grys') info.printerProfileColorSpace = 'grayscale';
211
220
  if (desc['Nm '] !== undefined) info.printerProfile = desc['Nm '];
212
221
  if (desc.MpBl !== undefined) info.blackPointCompensation = desc.MpBl;
213
222
  if (desc.printSixteenBit !== undefined) info.printSixteenBit = desc.printSixteenBit;
@@ -233,13 +242,13 @@ addHandler(
233
242
  desc.PstS = true;
234
243
  } else {
235
244
  if (info.hardProof !== undefined) desc.hardProof = !!info.hardProof;
236
- desc.ClrS = 'ClrS.RGBC'; // TODO: ???
237
- desc['Nm '] = info.printerProfile ?? 'CIE RGB';
245
+ desc.ClrS = info.printerProfileColorSpace === 'grayscale' ? 'ClrS.Grys' : 'ClrS.RGBC';
246
+ if (info.printerProfile !== undefined) desc['Nm '] = info.printerProfile;
238
247
  }
239
248
 
240
249
  desc.Inte = Inte.encode(info.renderingIntent);
241
250
 
242
- if (!info.printerManagesColors) desc.MpBl = !!info.blackPointCompensation;
251
+ if (!info.printerManagesColors && info.blackPointCompensation !== undefined) desc.MpBl = !!info.blackPointCompensation;
243
252
 
244
253
  desc.printSixteenBit = !!info.printSixteenBit;
245
254
  desc.printerName = info.printerName || '';
@@ -251,9 +260,9 @@ addHandler(
251
260
  MpBl: !!info.proofSetup.blackPointCompensation,
252
261
  paperWhite: !!info.proofSetup.paperWhite,
253
262
  };
254
- } else {
263
+ } else if (info.proofSetup) {
255
264
  desc.printProofSetup = {
256
- Bltn: info.proofSetup?.builtin ? `builtinProof.${info.proofSetup.builtin}` : 'builtinProof.proofCMYK',
265
+ Bltn: `builtinProof.${info.proofSetup.builtin || 'proofCMYK'}`,
257
266
  };
258
267
  }
259
268
 
@@ -261,19 +270,101 @@ addHandler(
261
270
  },
262
271
  );
263
272
 
264
- MOCK_HANDLERS && addHandler(
273
+ interface PrintStyleDescriptor {
274
+ Cptn: boolean;
275
+ Clbr: boolean;
276
+ RgsM: boolean;
277
+ CrnC: boolean;
278
+ CntC: boolean;
279
+ Lbls: boolean;
280
+ Ngtv: boolean;
281
+ EmlD: boolean;
282
+ Intr: boolean;
283
+ Bckg: DescriptorColor;
284
+ BrdT: DescriptorUnitsValue;
285
+ 'Bld ': DescriptorUnitsValue;
286
+ Rslt: DescriptorUnitsValue;
287
+ vectorData: boolean;
288
+ PgPs: string;
289
+ Left: DescriptorUnitsValue;
290
+ 'Top ': DescriptorUnitsValue;
291
+ 'Scl ': DescriptorUnitsValue;
292
+ cropWhenPrinting?: boolean;
293
+ cropRectBottom?: number;
294
+ cropRectLeft?: number;
295
+ cropRectRight?: number;
296
+ cropRectTop?: number;
297
+ }
298
+
299
+ addHandler(
265
300
  1083, // Print style
266
- target => (target as any)._ir1083 !== undefined,
267
- (reader, target, left) => {
268
- LOG_MOCK_HANDLERS && console.log('image resource 1083', left());
269
- (target as any)._ir1083 = readBytes(reader, left());
301
+ target => target.printStyle !== undefined,
302
+ (reader, target) => {
303
+ const desc: PrintStyleDescriptor = readVersionAndDescriptor(reader);
304
+
305
+ target.printStyle = {
306
+ caption: desc.Cptn,
307
+ calibrationBars: desc.Clbr,
308
+ registrationMarks: desc.RgsM,
309
+ cornerCropMarks: desc.CrnC,
310
+ centerCropMarks: desc.CntC,
311
+ labels: desc.Lbls,
312
+ negative: desc.Ngtv,
313
+ emulsionDown: desc.EmlD,
314
+ interpolate: desc.Intr,
315
+ backgroundColor: parseColor(desc.Bckg),
316
+ border: parseUnits(desc.BrdT),
317
+ bleed: parseUnits(desc['Bld ']),
318
+ resolution: parseUnits(desc.Rslt),
319
+ vectorData: desc.vectorData,
320
+ pagePosition: desc.PgPs === 'PgPs.PgPC' ? 'centered' : desc.PgPs,
321
+ left: parseUnits(desc.Left),
322
+ top: parseUnits(desc['Top ']),
323
+ scale: parsePercent(desc['Scl ']),
324
+ };
270
325
 
271
- // TODO:
272
- // const desc = readVersionAndDescriptor(reader);
273
- // console.log('1083', require('util').inspect(desc, false, 99, true));
326
+ if (desc.cropWhenPrinting !== undefined) {
327
+ target.printStyle.cropWhenPrinting = desc.cropWhenPrinting;
328
+ target.printStyle.cropRect = {
329
+ top: desc.cropRectTop || 0,
330
+ left: desc.cropRectLeft || 0,
331
+ bottom: desc.cropRectBottom || 0,
332
+ right: desc.cropRectRight || 0,
333
+ };
334
+ }
274
335
  },
275
336
  (writer, target) => {
276
- writeBytes(writer, (target as any)._ir1083);
337
+ const style = target.printStyle!;
338
+ const desc: PrintStyleDescriptor = {
339
+ Cptn: !!style.caption,
340
+ Clbr: !!style.calibrationBars,
341
+ RgsM: !!style.registrationMarks,
342
+ CrnC: !!style.cornerCropMarks,
343
+ CntC: !!style.centerCropMarks,
344
+ Lbls: !!style.labels,
345
+ Ngtv: !!style.negative,
346
+ EmlD: !!style.emulsionDown,
347
+ Intr: !!style.interpolate,
348
+ Bckg: serializeColor(style.backgroundColor || { r: 255, g: 255, b: 255 }),
349
+ BrdT: unitsValue(style.border || { units: 'Distance', value: 0 }, 'border'),
350
+ 'Bld ': unitsValue(style.bleed || { units: 'Distance', value: 0 }, 'bleed'),
351
+ Rslt: unitsValue(style.resolution || { units: 'Pixels', value: 72 }, 'resolution'),
352
+ vectorData: style.vectorData ?? true,
353
+ PgPs: (!style.pagePosition || style.pagePosition === 'centered') ? 'PgPs.PgPC' : style.pagePosition,
354
+ Left: unitsValue(style.left || { units: 'Distance', value: 0 }, 'left'),
355
+ 'Top ': unitsValue(style.top || { units: 'Distance', value: 0 }, 'top'),
356
+ 'Scl ': unitsPercentF(style.scale ?? 1),
357
+ };
358
+
359
+ if (style.cropWhenPrinting !== undefined || style.cropRect) {
360
+ desc.cropWhenPrinting = !!style.cropWhenPrinting;
361
+ desc.cropRectBottom = style.cropRect?.bottom ?? 0;
362
+ desc.cropRectLeft = style.cropRect?.left ?? 0;
363
+ desc.cropRectRight = style.cropRect?.right ?? 0;
364
+ desc.cropRectTop = style.cropRect?.top ?? 0;
365
+ }
366
+
367
+ writeVersionAndDescriptor(writer, '', 'printOutputOptions', desc);
277
368
  },
278
369
  );
279
370
 
@@ -372,15 +463,72 @@ addHandler(
372
463
  },
373
464
  );
374
465
 
375
- MOCK_HANDLERS && addHandler(
376
- 1077,
377
- target => (target as any)._ir1077 !== undefined,
466
+ const displayInfoKinds = ['selected', 'protected', 'spot'] as const;
467
+ const supportedColorSpaces: number[] = [ColorSpace.RGB, ColorSpace.HSB, ColorSpace.CMYK, ColorSpace.Lab, ColorSpace.Grayscale];
468
+
469
+ function readDisplayInfo(reader: PsdReader) {
470
+ const info: NonNullable<ImageResources['alphaChannelDisplayInfo']>[number] = { opacity: 0, kind: 'selected' };
471
+ const colorSpace = reader.view.getUint16(reader.offset);
472
+
473
+ if (supportedColorSpaces.indexOf(colorSpace) !== -1) {
474
+ info.color = readColor(reader);
475
+ } else {
476
+ // keep color in unknown color space as it is, so the channel info is not lost or misaligned
477
+ skipBytes(reader, 2);
478
+ info.rawColor = { colorSpace, values: [readUint16(reader), readUint16(reader), readUint16(reader), readUint16(reader)] };
479
+ }
480
+
481
+ info.opacity = readUint16(reader) / 100;
482
+ const kind = displayInfoKinds[readUint8(reader)];
483
+ if (!kind) throw new Error('Invalid display info kind');
484
+ info.kind = kind;
485
+ return info;
486
+ }
487
+
488
+ addHandler(
489
+ 1007, // Display info (obsolete, replaced by 1077), written back as 1077
490
+ () => false,
378
491
  (reader, target, left) => {
379
- LOG_MOCK_HANDLERS && console.log('image resource 1077', left());
380
- (target as any)._ir1077 = readBytes(reader, left());
492
+ const info: NonNullable<ImageResources['alphaChannelDisplayInfo']> = [];
493
+
494
+ while (left() >= 14) {
495
+ info.push(readDisplayInfo(reader));
496
+ skipBytes(reader, 1); // padding
497
+ }
498
+
499
+ // 1077 resource has more precise data and is used if present
500
+ if (!target.alphaChannelDisplayInfo) target.alphaChannelDisplayInfo = info;
501
+ },
502
+ () => { },
503
+ );
504
+
505
+ addHandler(
506
+ 1077, // Display info (alpha and spot channels)
507
+ target => target.alphaChannelDisplayInfo !== undefined,
508
+ (reader, target, left) => {
509
+ const version = readUint32(reader);
510
+ if (version !== 1) throw new Error(`Invalid display info version: ${version}`);
511
+
512
+ target.alphaChannelDisplayInfo = [];
513
+
514
+ while (left() >= 13) {
515
+ target.alphaChannelDisplayInfo.push(readDisplayInfo(reader));
516
+ }
381
517
  },
382
518
  (writer, target) => {
383
- writeBytes(writer, (target as any)._ir1077);
519
+ writeUint32(writer, 1); // version
520
+
521
+ for (const info of target.alphaChannelDisplayInfo!) {
522
+ if (!info.color && info.rawColor) {
523
+ writeUint16(writer, info.rawColor.colorSpace);
524
+ for (let i = 0; i < 4; i++) writeUint16(writer, info.rawColor.values[i] | 0);
525
+ } else {
526
+ writeColor(writer, info.color || { r: 255, g: 0, b: 0 });
527
+ }
528
+
529
+ writeUint16(writer, Math.round(clamp(info.opacity, 0, 1) * 100));
530
+ writeUint8(writer, Math.max(0, displayInfoKinds.indexOf(info.kind)));
531
+ }
384
532
  },
385
533
  );
386
534
 
@@ -474,42 +622,176 @@ addHandler(
474
622
  },
475
623
  );
476
624
 
477
- MOCK_HANDLERS && addHandler(
478
- 10000, // Print flags
479
- target => (target as any)._ir10000 !== undefined,
480
- (reader, target, left) => {
481
- LOG_MOCK_HANDLERS && console.log('image resource 10000', left());
482
- (target as any)._ir10000 = readBytes(reader, left());
625
+ addHandler(
626
+ 10000, // Print flags information
627
+ target => target.printFlagsInfo !== undefined,
628
+ (reader, target) => {
629
+ const version = readUint16(reader);
630
+ if (version !== 1) throw new Error(`Invalid print flags information version: ${version}`);
631
+ const centerCropMarks = !!readUint8(reader);
632
+ skipBytes(reader, 1);
633
+ const bleedWidth = readUint32(reader);
634
+ const bleedWidthScale = readUint16(reader);
635
+ target.printFlagsInfo = { centerCropMarks, bleedWidth, bleedWidthScale };
483
636
  },
484
637
  (writer, target) => {
485
- writeBytes(writer, (target as any)._ir10000);
638
+ const info = target.printFlagsInfo!;
639
+ writeUint16(writer, 1); // version
640
+ writeUint8(writer, info.centerCropMarks ? 1 : 0);
641
+ writeUint8(writer, 0);
642
+ writeUint32(writer, info.bleedWidth | 0);
643
+ writeUint16(writer, info.bleedWidthScale | 0);
486
644
  },
487
645
  );
488
646
 
489
- MOCK_HANDLERS && addHandler(
490
- 1013, // Color halftoning
491
- target => (target as any)._ir1013 !== undefined,
492
- (reader, target, left) => {
493
- LOG_MOCK_HANDLERS && console.log('image resource 1013', left());
494
- (target as any)._ir1013 = readBytes(reader, left());
647
+ addHandler(
648
+ 1046, // Indexed color table count
649
+ target => target.indexedColorCount !== undefined,
650
+ (reader, target) => {
651
+ target.indexedColorCount = readUint16(reader);
495
652
  },
496
653
  (writer, target) => {
497
- writeBytes(writer, (target as any)._ir1013);
654
+ writeUint16(writer, target.indexedColorCount!);
498
655
  },
499
656
  );
500
657
 
501
- MOCK_HANDLERS && addHandler(
502
- 1016, // Color transfer functions
503
- target => (target as any)._ir1016 !== undefined,
658
+ addHandler(
659
+ 1047, // Transparency index
660
+ target => target.transparencyIndex !== undefined,
661
+ (reader, target) => {
662
+ target.transparencyIndex = readUint16(reader);
663
+ },
664
+ (writer, target) => {
665
+ writeUint16(writer, target.transparencyIndex!);
666
+ },
667
+ );
668
+
669
+ const halftoneFrequencyUnits: HalftoneScreen['frequencyUnits'][] = ['lines/inch', 'lines/cm']; // 1, 2
670
+ const halftoneShapes: (HalftoneScreen['shape'] | undefined)[] = ['round', 'ellipse', 'line', 'square', 'cross', undefined, 'diamond']; // 0 - 6
671
+
672
+ function readHalftoneScreens(reader: PsdReader, left: () => number) {
673
+ const screens: HalftoneScreen[] = [];
674
+
675
+ while (left() >= 18) {
676
+ const frequency = readFixedPoint32(reader);
677
+ const frequencyUnits = halftoneFrequencyUnits[readUint16(reader) - 1];
678
+ if (!frequencyUnits) throw new Error('Invalid halftone screen frequency units');
679
+ const angle = readFixedPoint32(reader);
680
+ const shape = halftoneShapes[readInt16(reader)];
681
+ if (!shape) throw new Error('Invalid halftone screen shape');
682
+ skipBytes(reader, 4);
683
+ const accurateScreens = !!readUint8(reader);
684
+ const printerDefaultScreens = !!readUint8(reader);
685
+ screens.push({ frequency, frequencyUnits, angle, shape, accurateScreens, printerDefaultScreens });
686
+ }
687
+
688
+ return screens;
689
+ }
690
+
691
+ function writeHalftoneScreens(writer: PsdWriter, screens: HalftoneScreen[]) {
692
+ for (const screen of screens) {
693
+ writeFixedPoint32(writer, screen.frequency);
694
+ writeUint16(writer, Math.max(0, halftoneFrequencyUnits.indexOf(screen.frequencyUnits)) + 1);
695
+ writeFixedPoint32(writer, screen.angle);
696
+ writeInt16(writer, Math.max(0, halftoneShapes.indexOf(screen.shape)));
697
+ writeZeros(writer, 4);
698
+ writeUint8(writer, screen.accurateScreens ? 1 : 0);
699
+ writeUint8(writer, screen.printerDefaultScreens ? 1 : 0);
700
+ }
701
+ }
702
+
703
+ addHandler(
704
+ 1012, // Grayscale and multichannel halftoning information
705
+ target => target.grayscaleHalftoneScreens !== undefined,
706
+ (reader, target, left) => target.grayscaleHalftoneScreens = readHalftoneScreens(reader, left),
707
+ (writer, target) => writeHalftoneScreens(writer, target.grayscaleHalftoneScreens!),
708
+ );
709
+
710
+ addHandler(
711
+ 1013, // Color halftoning information
712
+ target => target.colorHalftoneScreens !== undefined,
713
+ (reader, target, left) => target.colorHalftoneScreens = readHalftoneScreens(reader, left),
714
+ (writer, target) => writeHalftoneScreens(writer, target.colorHalftoneScreens!),
715
+ );
716
+
717
+ function readTransferFunctions(reader: PsdReader, left: () => number) {
718
+ const functions: PrintTransferFunction[] = [];
719
+
720
+ while (left() >= 28) {
721
+ const curve: number[] = [];
722
+ for (let i = 0; i < 13; i++) curve.push(readInt16(reader));
723
+ const override = !!readUint16(reader);
724
+ functions.push({ curve, override });
725
+ }
726
+
727
+ return functions;
728
+ }
729
+
730
+ function writeTransferFunctions(writer: PsdWriter, functions: PrintTransferFunction[]) {
731
+ for (const f of functions) {
732
+ for (let i = 0; i < 13; i++) writeInt16(writer, f.curve[i] ?? -1);
733
+ writeUint16(writer, f.override ? 1 : 0);
734
+ }
735
+ }
736
+
737
+ addHandler(
738
+ 1014, // Duotone halftoning information
739
+ target => target.duotoneHalftoneScreens !== undefined,
740
+ (reader, target, left) => target.duotoneHalftoneScreens = readHalftoneScreens(reader, left),
741
+ (writer, target) => writeHalftoneScreens(writer, target.duotoneHalftoneScreens!),
742
+ );
743
+
744
+ addHandler(
745
+ 1017, // Duotone transfer functions
746
+ target => target.duotoneTransferFunctions !== undefined,
747
+ (reader, target, left) => target.duotoneTransferFunctions = readTransferFunctions(reader, left),
748
+ (writer, target) => writeTransferFunctions(writer, target.duotoneTransferFunctions!),
749
+ );
750
+
751
+ addHandler(
752
+ 1018, // Duotone image information
753
+ target => target.duotoneImageInformation !== undefined,
754
+ (reader, target, left) => target.duotoneImageInformation = readBytes(reader, left()),
755
+ (writer, target) => writeBytes(writer, target.duotoneImageInformation),
756
+ );
757
+
758
+ addHandler(
759
+ 1066, // Alternate duotone colors
760
+ target => target.alternateDuotoneColors !== undefined,
504
761
  (reader, target, left) => {
505
- LOG_MOCK_HANDLERS && console.log('image resource 1016', left());
506
- (target as any)._ir1016 = readBytes(reader, left());
762
+ if (readUint16(reader) !== 1) throw new Error('Invalid alternate duotone colors version');
763
+ const colors: Color[] = [];
764
+ const count = readUint16(reader);
765
+ for (let i = 0; i < count && left() >= 10; i++) colors.push(readColor(reader));
766
+ const labCount = readUint16(reader);
767
+ if (labCount * 3 > left()) throw new Error('Invalid alternate duotone colors Lab table size');
768
+ const labColors = readBytes(reader, labCount * 3);
769
+ target.alternateDuotoneColors = { colors, labColors };
507
770
  },
508
771
  (writer, target) => {
509
- writeBytes(writer, (target as any)._ir1016);
772
+ const { colors, labColors } = target.alternateDuotoneColors!;
773
+ writeUint16(writer, 1); // version
774
+ writeUint16(writer, colors.length);
775
+ for (const color of colors) writeColor(writer, color);
776
+ writeUint16(writer, Math.floor(labColors.byteLength / 3));
777
+ writeBytes(writer, labColors.subarray(0, Math.floor(labColors.byteLength / 3) * 3));
510
778
  },
511
779
  );
512
780
 
781
+ addHandler(
782
+ 1015, // Grayscale and multichannel transfer function
783
+ target => target.grayscaleTransferFunctions !== undefined,
784
+ (reader, target, left) => target.grayscaleTransferFunctions = readTransferFunctions(reader, left),
785
+ (writer, target) => writeTransferFunctions(writer, target.grayscaleTransferFunctions!),
786
+ );
787
+
788
+ addHandler(
789
+ 1016, // Color transfer functions
790
+ target => target.colorTransferFunctions !== undefined,
791
+ (reader, target, left) => target.colorTransferFunctions = readTransferFunctions(reader, left),
792
+ (writer, target) => writeTransferFunctions(writer, target.colorTransferFunctions!),
793
+ );
794
+
513
795
  interface CountInformationDesc {
514
796
  Vrsn: 1;
515
797
  countGroupList: {
@@ -631,17 +913,20 @@ addHandler(
631
913
 
632
914
  if (version !== 1) throw new Error(`Invalid 1032 resource version: ${version}`);
633
915
 
634
- target.gridAndGuidesInformation = {
635
- grid: { horizontal, vertical },
636
- guides: [],
637
- };
916
+ const previous = target.gridAndGuidesInformation?.guides; // in case guides were already read from 1092
917
+ const guides: Guide[] = [];
638
918
 
639
919
  for (let i = 0; i < count; i++) {
640
- target.gridAndGuidesInformation.guides!.push({
920
+ const guide: Guide = {
641
921
  location: readInt32(reader) / 32,
642
922
  direction: readUint8(reader) ? 'horizontal' : 'vertical'
643
- });
923
+ };
924
+ if (previous?.length === count && previous[i].color) guide.color = previous[i].color;
925
+ if (previous?.length === count && previous[i].id) guide.id = previous[i].id;
926
+ guides.push(guide);
644
927
  }
928
+
929
+ target.gridAndGuidesInformation = { grid: { horizontal, vertical }, guides };
645
930
  },
646
931
  (writer, target) => {
647
932
  const info = target.gridAndGuidesInformation!;
@@ -660,6 +945,91 @@ addHandler(
660
945
  },
661
946
  );
662
947
 
948
+ type Guide = NonNullable<NonNullable<ImageResources['gridAndGuidesInformation']>['guides']>[number];
949
+
950
+ const defaultGuideColor = { r: 74, g: 255, b: 255 }; // Photoshop default (cyan)
951
+
952
+ addHandler(
953
+ 1092, // Grid and guides information with guide colors (same as 1032 with color of each guide)
954
+ target => target.gridAndGuidesInformation !== undefined,
955
+ (reader, target) => {
956
+ const version = readUint32(reader);
957
+ if (version !== 2) throw new Error(`Invalid 1092 resource version: ${version}`);
958
+ const horizontal = readUint32(reader);
959
+ const vertical = readUint32(reader);
960
+ const count = readUint32(reader);
961
+ const guides: Guide[] = [];
962
+
963
+ for (let i = 0; i < count; i++) {
964
+ const location = readInt32(reader) / 32;
965
+ const direction = readUint8(reader) ? 'horizontal' : 'vertical';
966
+ skipBytes(reader, 1);
967
+ const color = { r: readUint8(reader), g: readUint8(reader), b: readUint8(reader) };
968
+ guides.push({ location, direction, color });
969
+ }
970
+
971
+ const existing = target.gridAndGuidesInformation?.guides;
972
+
973
+ if (existing && existing.length === count) {
974
+ existing.forEach((guide, i) => guide.color = guides[i].color);
975
+ } else {
976
+ target.gridAndGuidesInformation = { grid: { horizontal, vertical }, guides };
977
+ }
978
+ },
979
+ (writer, target) => {
980
+ const info = target.gridAndGuidesInformation!;
981
+ const grid = info.grid || { horizontal: 18 * 32, vertical: 18 * 32 };
982
+ const guides = info.guides || [];
983
+
984
+ writeUint32(writer, 2); // version
985
+ writeUint32(writer, Math.round(grid.horizontal));
986
+ writeUint32(writer, Math.round(grid.vertical));
987
+ writeUint32(writer, guides.length);
988
+
989
+ for (const g of guides) {
990
+ const color = g.color || defaultGuideColor;
991
+ writeInt32(writer, Math.round(g.location * 32));
992
+ writeUint8(writer, g.direction === 'horizontal' ? 1 : 0);
993
+ writeUint8(writer, 0);
994
+ writeUint8(writer, Math.round(color.r));
995
+ writeUint8(writer, Math.round(color.g));
996
+ writeUint8(writer, Math.round(color.b));
997
+ }
998
+ },
999
+ );
1000
+
1001
+ const emptyGuid = '00000000-0000-0000-0000-000000000000';
1002
+
1003
+ addHandler(
1004
+ 1097, // Guide ids
1005
+ target => !!target.gridAndGuidesInformation && (target.gridAndGuidesInformation.guides || []).every(g => g.id),
1006
+ (reader, target) => {
1007
+ const count = readUint32(reader);
1008
+ const ids: string[] = [];
1009
+
1010
+ for (let i = 0; i < count; i++) {
1011
+ ids.push(readAsciiString(reader, readUint32(reader)));
1012
+ readUint32(reader); // unknown, always 0
1013
+ readAsciiString(reader, readUint32(reader)); // unknown, always empty guid
1014
+ }
1015
+
1016
+ const guides = target.gridAndGuidesInformation?.guides;
1017
+ if (guides && guides.length === count) guides.forEach((guide, i) => guide.id = ids[i]);
1018
+ },
1019
+ (writer, target) => {
1020
+ const guides = target.gridAndGuidesInformation!.guides || [];
1021
+ writeUint32(writer, guides.length);
1022
+
1023
+ for (const g of guides) {
1024
+ writeUint32(writer, g.id!.length);
1025
+ writeAsciiString(writer, g.id!);
1026
+ writeUint32(writer, 0);
1027
+ writeUint32(writer, emptyGuid.length);
1028
+ writeAsciiString(writer, emptyGuid);
1029
+ }
1030
+ },
1031
+ );
1032
+
663
1033
  interface LayerCompsDescriptor {
664
1034
  list: {
665
1035
  _classID: 'Comp';
@@ -835,7 +1205,7 @@ addHandler(
835
1205
  workOutTime: frac(desc.workOutTime),
836
1206
  repeats: desc.LCnt,
837
1207
  hasMotion: desc.hasMotion,
838
- globalTracks: parseTrackList(desc.globalTrackList, !!reader.logMissingFeatures),
1208
+ globalTracks: parseTrackList(desc.globalTrackList, reader),
839
1209
  };
840
1210
 
841
1211
  if (desc.audioClipGroupList?.audioClipGroupList?.length) {
@@ -875,7 +1245,7 @@ addHandler(
875
1245
  workInTime: timeline.workInTime,
876
1246
  workOutTime: timeline.workOutTime,
877
1247
  LCnt: timeline.repeats,
878
- globalTrackList: serializeTrackList(timeline.globalTracks),
1248
+ globalTrackList: serializeTrackList(timeline.globalTracks, writer),
879
1249
  audioClipGroupList: {
880
1250
  audioClipGroupList: (timeline.audioClipGroups ?? []).map(a => ({
881
1251
  groupID: a.id,
@@ -1254,16 +1624,14 @@ addHandler(
1254
1624
  },
1255
1625
  );
1256
1626
 
1257
- MOCK_HANDLERS && addHandler(
1627
+ addHandler(
1258
1628
  1039, // ICC Profile
1259
- target => (target as any)._ir1039 !== undefined,
1629
+ target => target.iccProfile !== undefined,
1260
1630
  (reader, target, left) => {
1261
- // TODO: this is raw bytes, just return as a byte array
1262
- LOG_MOCK_HANDLERS && console.log('image resource 1039', left());
1263
- (target as any)._ir1039 = readBytes(reader, left());
1631
+ target.iccProfile = readBytes(reader, left());
1264
1632
  },
1265
1633
  (writer, target) => {
1266
- writeBytes(writer, (target as any)._ir1039);
1634
+ writeBytes(writer, target.iccProfile!);
1267
1635
  },
1268
1636
  );
1269
1637
 
@@ -1274,6 +1642,106 @@ addHandler(
1274
1642
  (writer, target) => writeUint32(writer, target.idsSeedNumber!),
1275
1643
  );
1276
1644
 
1645
+ addHandler(
1646
+ 1038, // Color samplers (Photoshop 5.0, replaced by 1073 in CS3)
1647
+ target => target.colorSamplers !== undefined && !target.colorSamplers.some(s => s.depth !== undefined),
1648
+ (reader, target, left) => {
1649
+ const version = readUint32(reader);
1650
+ if (version !== 1) throw new Error(`Invalid color samplers version: ${version}`);
1651
+ if (target.colorSamplers?.some(s => s.depth !== undefined)) return skipBytes(reader, left()); // already read from 1073
1652
+ const count = readUint32(reader);
1653
+ target.colorSamplers = [];
1654
+
1655
+ for (let i = 0; i < count && left() >= 10; i++) {
1656
+ // vertical and horizontal position in 1/16 pixel (inferred from sample files, the specification describes
1657
+ // these values as horizontal and vertical fixed point numbers)
1658
+ const y = readInt32(reader) / 16;
1659
+ const x = readInt32(reader) / 16;
1660
+ const colorSpace = readInt16(reader);
1661
+ target.colorSamplers.push({ x, y, colorSpace });
1662
+ }
1663
+ },
1664
+ (writer, target) => {
1665
+ writeUint32(writer, 1); // version
1666
+ writeUint32(writer, target.colorSamplers!.length);
1667
+
1668
+ for (const sampler of target.colorSamplers!) {
1669
+ writeInt32(writer, Math.round(sampler.y * 16));
1670
+ writeInt32(writer, Math.round(sampler.x * 16));
1671
+ writeInt16(writer, sampler.colorSpace);
1672
+ }
1673
+ },
1674
+ );
1675
+
1676
+ addHandler(
1677
+ 1073, // Color samplers (Photoshop CS3)
1678
+ target => target.colorSamplers !== undefined && target.colorSamplers.some(s => s.depth !== undefined),
1679
+ (reader, target, left) => {
1680
+ const version = readUint32(reader);
1681
+ if (version < 1 || version > 3) throw new Error(`Invalid color samplers version: ${version}`);
1682
+ const count = readUint32(reader);
1683
+ target.colorSamplers = [];
1684
+
1685
+ for (let i = 0; i < count && left() >= 10; i++) {
1686
+ // position is fixed point value for sampler version 1 (stored the same way as in 1038 resource) or float for version 2
1687
+ const samplerVersion = version === 3 ? readUint32(reader) : version;
1688
+ const y = samplerVersion === 1 ? readInt32(reader) / 16 : readFloat32(reader);
1689
+ const x = samplerVersion === 1 ? readInt32(reader) / 16 : readFloat32(reader);
1690
+ const colorSpace = readInt16(reader);
1691
+ const depth = version >= 2 ? readInt16(reader) : 1;
1692
+ target.colorSamplers.push({ x, y, colorSpace, depth });
1693
+ }
1694
+ },
1695
+ (writer, target) => {
1696
+ writeUint32(writer, 3); // version
1697
+ writeUint32(writer, target.colorSamplers!.length);
1698
+
1699
+ for (const sampler of target.colorSamplers!) {
1700
+ writeUint32(writer, 1); // sampler version
1701
+ writeInt32(writer, Math.round(sampler.y * 16));
1702
+ writeInt32(writer, Math.round(sampler.x * 16));
1703
+ writeInt16(writer, sampler.colorSpace);
1704
+ writeInt16(writer, sampler.depth ?? 1);
1705
+ }
1706
+ },
1707
+ );
1708
+
1709
+ addHandler(
1710
+ 1022, // Quick Mask information
1711
+ target => target.quickMaskInfo !== undefined,
1712
+ (reader, target) => {
1713
+ const channelId = readUint16(reader);
1714
+ const initiallyEmpty = !!readUint8(reader);
1715
+ target.quickMaskInfo = { channelId, initiallyEmpty };
1716
+ },
1717
+ (writer, target) => {
1718
+ writeUint16(writer, target.quickMaskInfo!.channelId);
1719
+ writeUint8(writer, target.quickMaskInfo!.initiallyEmpty ? 1 : 0);
1720
+ },
1721
+ );
1722
+
1723
+ addHandler(
1724
+ 1074, // Measurement scale
1725
+ target => target.measurementScale !== undefined,
1726
+ (reader, target, left) => {
1727
+ target.measurementScale = readBytes(reader, left());
1728
+ },
1729
+ (writer, target) => {
1730
+ writeBytes(writer, target.measurementScale);
1731
+ },
1732
+ );
1733
+
1734
+ addHandler(
1735
+ 1042, // Effects visible (only present when effects are hidden)
1736
+ target => target.effectsVisible !== undefined,
1737
+ (reader, target) => {
1738
+ target.effectsVisible = !!readUint8(reader);
1739
+ },
1740
+ (writer, target) => {
1741
+ writeUint8(writer, target.effectsVisible ? 1 : 0);
1742
+ },
1743
+ );
1744
+
1277
1745
  addHandler(
1278
1746
  1036,
1279
1747
  target => target.thumbnail !== undefined || target.thumbnailRaw !== undefined,
@@ -1281,14 +1749,39 @@ addHandler(
1281
1749
  const format = readUint32(reader); // 1 = kJpegRGB, 0 = kRawRGB
1282
1750
  const width = readUint32(reader);
1283
1751
  const height = readUint32(reader);
1284
- readUint32(reader); // widthBytes = (width * bits_per_pixel + 31) / 32 * 4.
1752
+ const widthBytes = readUint32(reader); // widthBytes = (width * bits_per_pixel + 31) / 32 * 4.
1285
1753
  readUint32(reader); // totalSize = widthBytes * height * planes
1286
1754
  readUint32(reader); // sizeAfterCompression
1287
1755
  const bitsPerPixel = readUint16(reader); // 24
1288
- const planes = readUint16(reader); // 1
1756
+ readUint16(reader); // planes, should be 1 (some applications write 3)
1757
+
1758
+ if (format === 0 && bitsPerPixel === 24 && widthBytes >= width * 3 && widthBytes * height <= left()) {
1759
+ // raw RGB data, rows are padded to 4 bytes
1760
+ if (reader.useRawThumbnail) {
1761
+ reader.logMissingFeatures && reader.log('Raw thumbnail data is not supported with useRawThumbnail option');
1762
+ skipBytes(reader, left());
1763
+ return;
1764
+ }
1765
+
1766
+ const data = readBytes(reader, widthBytes * height);
1767
+ const imageData = createImageData(width, height);
1768
+
1769
+ for (let y = 0, dst = 0; y < height; y++) {
1770
+ for (let x = 0, src = y * widthBytes; x < width; x++, src += 3, dst += 4) {
1771
+ imageData.data[dst + 0] = data[src + 0];
1772
+ imageData.data[dst + 1] = data[src + 1];
1773
+ imageData.data[dst + 2] = data[src + 2];
1774
+ imageData.data[dst + 3] = 255;
1775
+ }
1776
+ }
1777
+
1778
+ target.thumbnail = imageDataToCanvas(imageData);
1779
+ skipBytes(reader, left());
1780
+ return;
1781
+ }
1289
1782
 
1290
- if (format !== 1 || bitsPerPixel !== 24 || planes !== 1) {
1291
- reader.logMissingFeatures && reader.log(`Invalid thumbnail data (format: ${format}, bitsPerPixel: ${bitsPerPixel}, planes: ${planes})`);
1783
+ if (format !== 1 || bitsPerPixel !== 24) {
1784
+ reader.logMissingFeatures && reader.log(`Invalid thumbnail data (format: ${format}, bitsPerPixel: ${bitsPerPixel})`);
1292
1785
  skipBytes(reader, left());
1293
1786
  return;
1294
1787
  }
@@ -1367,15 +1860,14 @@ addHandler(
1367
1860
  },
1368
1861
  );
1369
1862
 
1370
- MOCK_HANDLERS && addHandler(
1371
- 1058, // EXIF data 1.
1372
- target => (target as any)._ir1058 !== undefined,
1863
+ addHandler(
1864
+ 1058, // EXIF data 1
1865
+ target => target.exifData !== undefined,
1373
1866
  (reader, target, left) => {
1374
- LOG_MOCK_HANDLERS && console.log('image resource 1058', left());
1375
- (target as any)._ir1058 = readBytes(reader, left());
1867
+ target.exifData = readBytes(reader, left());
1376
1868
  },
1377
1869
  (writer, target) => {
1378
- writeBytes(writer, (target as any)._ir1058);
1870
+ writeBytes(writer, target.exifData!);
1379
1871
  },
1380
1872
  );
1381
1873
 
@@ -1401,32 +1893,116 @@ addHandler(
1401
1893
  },
1402
1894
  );
1403
1895
 
1404
- interface Descriptor1088 {
1896
+ interface Descriptor1088Read {
1405
1897
  'null': string[];
1406
1898
  }
1407
1899
 
1900
+ interface Descriptor1088Write {
1901
+ 'null': DescriptorReferenceItem[];
1902
+ }
1903
+
1408
1904
  addHandler(
1409
1905
  1088,
1410
1906
  target => target.pathSelectionState !== undefined,
1411
1907
  (reader, target, _left) => {
1412
- const desc: Descriptor1088 = readVersionAndDescriptor(reader);
1908
+ const desc: Descriptor1088Read = readVersionAndDescriptor(reader);
1413
1909
  target.pathSelectionState = desc['null'];
1414
1910
  },
1415
1911
  (writer, target) => {
1416
- const desc: Descriptor1088 = { 'null': pathSelectionStateToReference(target.pathSelectionState!) };
1912
+ const desc: Descriptor1088Write = { 'null': pathSelectionStateToReference(target.pathSelectionState!) };
1417
1913
  writeVersionAndDescriptor(writer, '', 'null', desc);
1418
1914
  },
1419
1915
  );
1420
1916
 
1421
- MOCK_HANDLERS && addHandler(
1422
- 1025,
1423
- target => (target as any)._ir1025 !== undefined,
1917
+ addHandler(
1918
+ 1025, // Work path
1919
+ target => target.workPath !== undefined,
1920
+ (reader, target, left, { width, height }) => {
1921
+ target.workPath = { paths: [] };
1922
+ readVectorMask(reader, target.workPath, width, height, left());
1923
+ skipBytes(reader, left());
1924
+ },
1925
+ (writer, target, _index, { width, height }) => {
1926
+ writeVectorMask(writer, target.workPath!, width, height);
1927
+ },
1928
+ );
1929
+
1930
+ const MAX_SAVED_PATHS = 999; // ids 2000-2998
1931
+
1932
+ addHandler(
1933
+ 2000, // Saved paths (one resource for each path, named after the path)
1934
+ target => target.savedPaths ? Math.min(target.savedPaths.length, MAX_SAVED_PATHS) : 0,
1935
+ (reader, target, left, { signature, id, name, width, height }) => {
1936
+ if (signature !== '8BIM') { // ImageReady ('MeSa') resources use the same ids for other data
1937
+ reader.logMissingFeatures && reader.log(`Unhandled image resource: ${signature} ${id}`);
1938
+ skipBytes(reader, left());
1939
+ return;
1940
+ }
1941
+
1942
+ const savedPaths = target.savedPaths ??= [];
1943
+ const path: SavedPath = { name: decodeEncodedString(reader, name), paths: [] };
1944
+ readVectorMask(reader, path, width, height, left());
1945
+ skipBytes(reader, left());
1946
+
1947
+ const keyDescriptorList = target.savedPathsOrigination?.[savedPaths.length];
1948
+ if (keyDescriptorList) path.vectorOrigination = { keyDescriptorList };
1949
+
1950
+ savedPaths.push(path);
1951
+ },
1952
+ (writer, target, index, { width, height }) => {
1953
+ writeVectorMask(writer, target.savedPaths![index], width, height);
1954
+ },
1955
+ { keyCount: MAX_SAVED_PATHS, name: (target, index) => toShortAsciiString(target.savedPaths![index].name) },
1956
+ );
1957
+
1958
+ interface OriginPathInfoDescriptor {
1959
+ keyRootDescriptorList: KeyDescriptorListDescriptor[];
1960
+ }
1961
+
1962
+ addHandler(
1963
+ 3000, // Origin path info (origination data of saved paths)
1964
+ target => !!target.savedPaths?.some(path => path.vectorOrigination),
1424
1965
  (reader, target, left) => {
1425
- LOG_MOCK_HANDLERS && console.log('image resource 1025', left());
1426
- (target as any)._ir1025 = readBytes(reader, left());
1966
+ const desc = readVersionAndDescriptor(reader) as OriginPathInfoDescriptor;
1967
+
1968
+ desc.keyRootDescriptorList.forEach((item, i) => {
1969
+ const keyDescriptorList = parseKeyDescriptorList(item);
1970
+ if (!keyDescriptorList.length) return; // written for paths without origination data
1971
+
1972
+ const path = target.savedPaths?.[i];
1973
+
1974
+ if (path) {
1975
+ path.vectorOrigination = { keyDescriptorList };
1976
+ } else { // saved paths are stored after this resource
1977
+ (target.savedPathsOrigination ??= [])[i] = keyDescriptorList;
1978
+ }
1979
+ });
1980
+
1981
+ skipBytes(reader, left());
1427
1982
  },
1428
1983
  (writer, target) => {
1429
- writeBytes(writer, (target as any)._ir1025);
1984
+ const desc: OriginPathInfoDescriptor = {
1985
+ keyRootDescriptorList: target.savedPaths!.slice(0, MAX_SAVED_PATHS)
1986
+ .map(path => serializeKeyDescriptorList(path.vectorOrigination?.keyDescriptorList ?? [])),
1987
+ };
1988
+ writeVersionAndDescriptor(writer, '', 'null', desc);
1989
+ },
1990
+ { name: () => 'OriginDataIRB' },
1991
+ );
1992
+
1993
+ addHandler(
1994
+ 2999, // Clipping path
1995
+ target => target.clippingPath !== undefined,
1996
+ (reader, target, left) => {
1997
+ const name = readEncodedString(reader);
1998
+ const flatness = readFixedPoint32(reader);
1999
+ skipBytes(reader, left()); // fill rule (ignored by Photoshop)
2000
+ target.clippingPath = { name, flatness };
2001
+ },
2002
+ (writer, target) => {
2003
+ writeEncodedString(writer, target.clippingPath!.name);
2004
+ writeFixedPoint32(writer, target.clippingPath!.flatness);
2005
+ writeUint16(writer, 1); // fill rule (ignored by Photoshop)
1430
2006
  },
1431
2007
  );
1432
2008
 
@@ -1456,13 +2032,28 @@ interface AnimationsDescriptor {
1456
2032
  FSts: AnimationDescriptor[];
1457
2033
  }
1458
2034
 
2035
+ // plug-in resources are written with consecutive ids starting at 4000, in the same order as Photoshop writes them
2036
+ function pluginResourceKeys(target: InternalImageResources) {
2037
+ const keys: string[] = [];
2038
+ if (target.outputSettings !== undefined) keys.push('mset');
2039
+ if (target.animations !== undefined) keys.push('mani', 'mfri');
2040
+ return keys;
2041
+ }
2042
+
1459
2043
  addHandler(
1460
- 4000, // Plug-In resource(s)
1461
- target => target.animations !== undefined,
2044
+ 4000, // Plug-In resource(s), identified by key stored in the data (ids 4000-4999)
2045
+ target => pluginResourceKeys(target).length,
1462
2046
  (reader, target, left) => {
1463
2047
  const key = readSignature(reader);
1464
2048
 
1465
- if (key === 'mani') {
2049
+ if (key === 'mset') {
2050
+ // Save for Web output settings
2051
+ target.outputSettings = readVersionAndDescriptor(reader, true);
2052
+ skipBytes(reader, left());
2053
+ } else if (key === 'mfri') {
2054
+ // frame count, active frame and animation count, same data as in animations (mani)
2055
+ skipBytes(reader, left());
2056
+ } else if (key === 'mani') {
1466
2057
  checkSignature(reader, 'IRFR');
1467
2058
  readSection(reader, 1, left => {
1468
2059
  while (left() > 0) {
@@ -1490,24 +2081,43 @@ addHandler(
1490
2081
 
1491
2082
  // console.log('#4000 AnDs', require('util').inspect(desc, false, 99, true));
1492
2083
  // console.log('#4000 AnDs:result', require('util').inspect(target.animations, false, 99, true));
2084
+ skipBytes(reader, left()); // padding
1493
2085
  } else if (key === 'Roll') {
1494
2086
  const bytes = readBytes(reader, left());
1495
2087
  reader.logDevFeatures && reader.log('#4000 Roll', bytes);
1496
2088
  } else {
1497
2089
  reader.logMissingFeatures && reader.log('Unhandled subsection in #4000', key);
2090
+ skipBytes(reader, left());
1498
2091
  }
1499
2092
  });
1500
2093
  }
1501
2094
  });
2095
+
2096
+ skipBytes(reader, left()); // unknown data after animations section
1502
2097
  } else if (key === 'mopt') {
1503
2098
  const bytes = readBytes(reader, left());
1504
2099
  reader.logDevFeatures && reader.log('#4000 mopt', bytes);
1505
2100
  } else {
1506
- reader.logMissingFeatures && reader.log('Unhandled key in #4000:', key);
2101
+ reader.logMissingFeatures && reader.log(`Unhandled plug-in resource key: ${key}`);
2102
+ skipBytes(reader, left());
1507
2103
  }
1508
2104
  },
1509
- (writer, target) => {
1510
- if (target.animations) {
2105
+ (writer, target, index) => {
2106
+ const key = pluginResourceKeys(target)[index];
2107
+
2108
+ if (key === 'mset') {
2109
+ writeSignature(writer, 'mset');
2110
+ writeVersionAndDescriptor(writer, '', 'null', target.outputSettings, outputSettingsRoot);
2111
+ } else if (key === 'mfri') {
2112
+ const animations = target.animations!;
2113
+ writeSignature(writer, 'mfri');
2114
+ writeUint32(writer, 2); // version
2115
+ writeUint32(writer, 16); // length
2116
+ writeUint32(writer, animations.frames.length);
2117
+ writeUint32(writer, animations.animations[0]?.activeFrame ?? 0);
2118
+ writeUint32(writer, animations.animations.length);
2119
+ writeUint32(writer, 0);
2120
+ } else if (key === 'mani') {
1511
2121
  writeSignature(writer, 'mani');
1512
2122
  writeSignature(writer, 'IRFR');
1513
2123
  writeSection(writer, 1, () => {
@@ -1553,49 +2163,6 @@ addHandler(
1553
2163
  });
1554
2164
  }
1555
2165
  },
2166
+ { keyCount: 1000 },
1556
2167
  );
1557
2168
 
1558
- // TODO: Unfinished
1559
- MOCK_HANDLERS && addHandler(
1560
- 4001, // Plug-In resource(s)
1561
- target => (target as any)._ir4001 !== undefined,
1562
- (reader, target, left) => {
1563
- if (MOCK_HANDLERS) {
1564
- LOG_MOCK_HANDLERS && console.log('image resource 4001', left());
1565
- (target as any)._ir4001 = readBytes(reader, left());
1566
- return;
1567
- }
1568
-
1569
- const key = readSignature(reader);
1570
-
1571
- if (key === 'mfri') {
1572
- const version = readUint32(reader);
1573
- if (version !== 2) throw new Error('Invalid mfri version');
1574
-
1575
- const length = readUint32(reader);
1576
- const bytes = readBytes(reader, length);
1577
- reader.logDevFeatures && reader.log('mfri', bytes);
1578
- } else if (key === 'mset') {
1579
- const desc = readVersionAndDescriptor(reader);
1580
- reader.logDevFeatures && reader.log('mset', desc);
1581
- } else {
1582
- reader.logMissingFeatures && reader.log('Unhandled key in #4001', key);
1583
- }
1584
- },
1585
- (writer, target) => {
1586
- writeBytes(writer, (target as any)._ir4001);
1587
- },
1588
- );
1589
-
1590
- // TODO: Unfinished
1591
- MOCK_HANDLERS && addHandler(
1592
- 4002, // Plug-In resource(s)
1593
- target => (target as any)._ir4002 !== undefined,
1594
- (reader, target, left) => {
1595
- LOG_MOCK_HANDLERS && console.log('image resource 4002', left());
1596
- (target as any)._ir4002 = readBytes(reader, left());
1597
- },
1598
- (writer, target) => {
1599
- writeBytes(writer, (target as any)._ir4002);
1600
- },
1601
- );