ag-psd 33.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 (86) hide show
  1. package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
  2. package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
  3. package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
  4. package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
  5. package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
  6. package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
  7. package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
  8. package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
  9. package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
  10. package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
  11. package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
  12. package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
  13. package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
  14. package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
  15. package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
  16. package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
  17. package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
  18. package/CHANGELOG.md +22 -0
  19. package/README.md +1 -1
  20. package/README_PSD.md +25 -9
  21. package/TODO.md +13 -84
  22. package/dist/abr.js +12 -7
  23. package/dist/abr.js.map +1 -1
  24. package/dist/additionalInfo.d.ts +2 -0
  25. package/dist/additionalInfo.js +361 -75
  26. package/dist/additionalInfo.js.map +1 -1
  27. package/dist/bundle.js +1524 -389
  28. package/dist/descriptor.d.ts +14 -5
  29. package/dist/descriptor.js +102 -64
  30. package/dist/descriptor.js.map +1 -1
  31. package/dist/helpers.d.ts +6 -0
  32. package/dist/helpers.js +242 -2
  33. package/dist/helpers.js.map +1 -1
  34. package/dist/imageResources.js +84 -3
  35. package/dist/imageResources.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/psd.d.ts +144 -2
  40. package/dist/psd.js.map +1 -1
  41. package/dist/psdReader.js +206 -110
  42. package/dist/psdReader.js.map +1 -1
  43. package/dist/psdWriter.d.ts +1 -0
  44. package/dist/psdWriter.js +336 -91
  45. package/dist/psdWriter.js.map +1 -1
  46. package/dist/text.d.ts +32 -1
  47. package/dist/text.js +58 -0
  48. package/dist/text.js.map +1 -1
  49. package/dist-es/abr.js +12 -7
  50. package/dist-es/abr.js.map +1 -1
  51. package/dist-es/additionalInfo.d.ts +2 -0
  52. package/dist-es/additionalInfo.js +361 -75
  53. package/dist-es/additionalInfo.js.map +1 -1
  54. package/dist-es/descriptor.d.ts +14 -5
  55. package/dist-es/descriptor.js +102 -64
  56. package/dist-es/descriptor.js.map +1 -1
  57. package/dist-es/helpers.d.ts +6 -0
  58. package/dist-es/helpers.js +242 -2
  59. package/dist-es/helpers.js.map +1 -1
  60. package/dist-es/imageResources.js +84 -3
  61. package/dist-es/imageResources.js.map +1 -1
  62. package/dist-es/index.d.ts +1 -0
  63. package/dist-es/index.js +1 -0
  64. package/dist-es/index.js.map +1 -1
  65. package/dist-es/psd.d.ts +144 -2
  66. package/dist-es/psd.js.map +1 -1
  67. package/dist-es/psdReader.js +206 -110
  68. package/dist-es/psdReader.js.map +1 -1
  69. package/dist-es/psdWriter.d.ts +1 -0
  70. package/dist-es/psdWriter.js +336 -91
  71. package/dist-es/psdWriter.js.map +1 -1
  72. package/dist-es/text.d.ts +32 -1
  73. package/dist-es/text.js +58 -0
  74. package/dist-es/text.js.map +1 -1
  75. package/package.json +1 -1
  76. package/perf.js +312 -9
  77. package/src/abr.ts +14 -7
  78. package/src/additionalInfo.ts +433 -87
  79. package/src/descriptor.ts +103 -58
  80. package/src/helpers.ts +262 -2
  81. package/src/imageResources.ts +107 -4
  82. package/src/index.ts +1 -0
  83. package/src/psd.ts +115 -2
  84. package/src/psdReader.ts +226 -108
  85. package/src/psdWriter.ts +345 -90
  86. package/src/text.ts +76 -1
package/src/psdWriter.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Psd, Layer, LayerAdditionalInfo, ColorMode, SectionDividerType, WriteOptions, Color, GlobalLayerMaskInfo, PixelData, LayerMaskData, Compression, ChannelID, PatternInfo } from './psd';
2
- import { hasAlpha, createCanvas, writeDataRLE, LayerChannelData, ChannelData, offsetForChannel, createImageData, fromBlendMode, clamp, LayerMaskFlags, MaskParams, ColorSpace, Bounds, largeAdditionalInfoKeys, writeDataZipWithoutPrediction, imageDataToCanvas, denormalizeLabAB } from './helpers';
2
+ import { hasAlpha, createCanvas, writeDataRLE, LayerChannelData, ChannelData, offsetForChannel, createImageData, fromBlendMode, clamp, LayerMaskFlags, MaskParams, ColorSpace, Bounds, largeAdditionalInfoKeys, writeDataZipWithoutPrediction, imageDataToCanvas, denormalizeLabAB, PlanarData, planarToBytes, writeDataRLEBytes, writeDataZipWithPrediction, rgbToCmykValues } from './helpers';
3
+ import { deflate as deflateSync } from 'pako';
3
4
  import { ExtendedWriteOptions, infoHandlers, infoHandlersMap } from './additionalInfo';
4
5
  import { InternalImageResources, resourceHandlers } from './imageResources';
5
6
  import { getCompositeImageData, getLayerImageData, getLayerMaskImageData, getLayerRealMaskImageData } from './psdReader';
@@ -9,6 +10,7 @@ export interface PsdWriter {
9
10
  buffer: ArrayBuffer;
10
11
  view: DataView;
11
12
  tempBuffer: Uint8Array | undefined;
13
+ logMissingFeatures?: boolean;
12
14
  log(...args: any[]): void;
13
15
  }
14
16
 
@@ -217,11 +219,38 @@ export function writeSection(writer: PsdWriter, round: number, func: () => void,
217
219
  function verifyPixelData(data: PixelData | undefined, name: string) {
218
220
  // check element size instead of instanceof, so arrays from other realms (iframes, workers, vm) are accepted
219
221
  const bytesPerElement = data && (data.data as any).BYTES_PER_ELEMENT;
220
- if (bytesPerElement !== undefined && bytesPerElement !== 1) {
221
- throw new Error(`${name} has incorrect bitDepth (only 8-bit Uint8Array or Uint8ClampedArray data is supported for writing)`);
222
+ if (bytesPerElement !== undefined && bytesPerElement !== 1 && bytesPerElement !== 2 && !(data!.data instanceof Float32Array)) {
223
+ throw new Error(`${name} has incorrect bitDepth (only Uint8Array, Uint8ClampedArray, Uint16Array and Float32Array data is supported for writing)`);
222
224
  }
223
225
  }
224
226
 
227
+ // supported bit depths for each color mode
228
+ const writeBitDepths: { [colorMode: number]: number[] | undefined; } = {
229
+ [ColorMode.Bitmap]: [1],
230
+ [ColorMode.Grayscale]: [8, 16, 32],
231
+ [ColorMode.Indexed]: [8],
232
+ [ColorMode.RGB]: [8, 16, 32],
233
+ [ColorMode.CMYK]: [8, 16],
234
+ };
235
+
236
+ const colorModeNames: { [colorMode: number]: string; } = {
237
+ [ColorMode.Bitmap]: 'Bitmap', [ColorMode.Grayscale]: 'Grayscale', [ColorMode.Indexed]: 'Indexed', [ColorMode.RGB]: 'RGB',
238
+ [ColorMode.CMYK]: 'CMYK', [ColorMode.Multichannel]: 'Multichannel', [ColorMode.Duotone]: 'Duotone', [ColorMode.Lab]: 'Lab',
239
+ };
240
+
241
+ function colorChannelsCount(colorMode: ColorMode) {
242
+ return colorMode === ColorMode.RGB ? 3 : (colorMode === ColorMode.CMYK ? 4 : 1);
243
+ }
244
+
245
+ // HDR toning settings that Photoshop stores in color mode data of 32bit documents (default values of new document),
246
+ // Photoshop refuses to open 32bit documents without them
247
+ const hdrToningData = [
248
+ 104, 100, 114, 116, 0, 0, 0, 3, 62, 107, 133, 31, 0, 0, 0, 2, 0, 0, 0, 7, 0, 76, 0, 105, 0, 110, 0, 101, 0, 97, 0, 114, 0, 0,
249
+ 0, 2, 0, 2, 0, 0, 0, 0, 0, 255, 0, 255, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 65, 128, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 63, 128, 0, 0,
250
+ 104, 100, 114, 97, 0, 0, 0, 6, 0, 0, 0, 0, 65, 160, 0, 0, 65, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 128, 0, 0, 0, 0, 0, 0,
251
+ 0, 0,
252
+ ];
253
+
225
254
  function verifyBitCount(target: Psd | Layer) {
226
255
  target.children?.forEach(verifyBitCount);
227
256
 
@@ -238,33 +267,38 @@ export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}
238
267
  if ((psd.width > 30000 || psd.height > 30000) && !options.psb)
239
268
  throw new Error('Document size is too large (max is 30000x30000, use PSB format instead)');
240
269
 
241
- const bitsPerChannel = psd.bitsPerChannel ?? 8;
270
+ const colorMode = psd.colorMode ?? ColorMode.RGB;
271
+ const bitsPerChannel = psd.bitsPerChannel ?? (colorMode === ColorMode.Bitmap ? 1 : 8);
272
+ const bitDepths = writeBitDepths[colorMode];
273
+
274
+ if (!bitDepths)
275
+ throw new Error(`${colorModeNames[colorMode] ?? colorMode} color mode is not supported for writing`);
276
+
277
+ if (bitDepths.indexOf(bitsPerChannel) === -1)
278
+ throw new Error(`bitsPerChannel ${bitsPerChannel} is not supported for writing in ${colorModeNames[colorMode]} color mode`);
279
+
280
+ // Photoshop doesn't support layers in bitmap and indexed color modes
281
+ const layersAllowed = colorMode !== ColorMode.Bitmap && colorMode !== ColorMode.Indexed;
282
+
283
+ if (!layersAllowed && psd.children?.length)
284
+ throw new Error(`Layers are not supported in ${colorModeNames[colorMode]} color mode`);
242
285
 
243
- if (bitsPerChannel !== 8)
244
- throw new Error('bitsPerChannel other than 8 are not supported for writing');
286
+ if (colorMode === ColorMode.Indexed && !psd.palette)
287
+ throw new Error('Missing color palette for Indexed color mode');
245
288
 
246
289
  if (options.log) writer.log = options.log;
290
+ if (options.logMissingFeatures) writer.logMissingFeatures = true;
247
291
 
248
292
  const layers: Layer[] = [];
249
293
  addChildren(layers, psd.children);
250
- if (!layers.length) layers.push({});
294
+ if (!layers.length && layersAllowed) layers.push({});
251
295
 
252
296
  verifyBitCount(psd);
253
297
 
254
298
  const imageResources: InternalImageResources = { ...psd.imageResources };
255
- const opt: ExtendedWriteOptions = { ...options, layerIds: new Set(), layerToId: new Map() };
256
-
257
- // document is always written in RGB color mode, skip resources specific to original color mode
258
- if (psd.colorMode !== undefined && psd.colorMode !== ColorMode.RGB) {
259
- delete imageResources.iccProfile;
260
- delete imageResources.indexedColorCount;
261
- delete imageResources.transparencyIndex;
262
- delete imageResources.alphaChannelDisplayInfo;
263
- delete imageResources.colorHalftoneScreens;
264
- delete imageResources.grayscaleHalftoneScreens;
265
- delete imageResources.colorTransferFunctions;
266
- delete imageResources.grayscaleTransferFunctions;
267
- }
299
+ const opt: ExtendedWriteOptions = { ...options, layerIds: new Set(), layerToId: new Map(), colorMode, bitsPerChannel };
300
+ // 16 and 32 bit documents store layers in Lr16 / Lr32 section of global additional info
301
+ const layersInGlobalInfo = bitsPerChannel === 16 || bitsPerChannel === 32;
268
302
 
269
303
  if (opt.generateThumbnail) {
270
304
  imageResources.thumbnail = createThumbnail(psd);
@@ -283,7 +317,7 @@ export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}
283
317
  if (imageData && (psd.width !== imageData.width || psd.height !== imageData.height))
284
318
  throw new Error('Document canvas must have the same size as document');
285
319
 
286
- const globalAlpha = !!imageData && hasAlpha(imageData);
320
+ const globalAlpha = !!imageData && layersAllowed && hasAlpha(imageData);
287
321
  // only used for encoding a single layer channel at a time, composite image is encoded directly into the writer buffer
288
322
  writer.tempBuffer = new Uint8Array(getLargestLayerSize(psd.children, !!options.psb));
289
323
 
@@ -291,14 +325,23 @@ export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}
291
325
  writeSignature(writer, '8BPS');
292
326
  writeUint16(writer, options.psb ? 2 : 1); // version
293
327
  writeZeros(writer, 6);
294
- writeUint16(writer, globalAlpha ? 4 : 3); // channels
328
+ writeUint16(writer, colorChannelsCount(colorMode) + (globalAlpha ? 1 : 0)); // channels
295
329
  writeUint32(writer, psd.height);
296
330
  writeUint32(writer, psd.width);
297
331
  writeUint16(writer, bitsPerChannel); // bits per channel
298
- writeUint16(writer, ColorMode.RGB); // we only support saving RGB right now
332
+ writeUint16(writer, colorMode);
299
333
 
300
- // color mode data (empty for RGB color mode)
301
- writeUint32(writer, 0);
334
+ // color mode data
335
+ writeSection(writer, 1, () => {
336
+ if (colorMode === ColorMode.Indexed) {
337
+ const palette = psd.palette!;
338
+ for (let i = 0; i < 256; i++) writeUint8(writer, palette[i]?.r ?? 0);
339
+ for (let i = 0; i < 256; i++) writeUint8(writer, palette[i]?.g ?? 0);
340
+ for (let i = 0; i < 256; i++) writeUint8(writer, palette[i]?.b ?? 0);
341
+ } else if (bitsPerChannel === 32) {
342
+ writeBytes(writer, new Uint8Array(hdrToningData));
343
+ }
344
+ });
302
345
 
303
346
  // image resources
304
347
 
@@ -322,24 +365,217 @@ export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}
322
365
 
323
366
  // layer and mask info
324
367
  writeSection(writer, 2, () => {
325
- writeLayerInfo(writer, layers, psd, globalAlpha, opt);
368
+ if (layersInGlobalInfo || !layersAllowed) {
369
+ writeSection(writer, 4, () => { }, true, opt.psb); // empty layer info
370
+ } else {
371
+ writeLayerInfo(writer, layers, psd, globalAlpha, opt);
372
+ }
373
+
326
374
  writeGlobalLayerMaskInfo(writer, psd.globalLayerMaskInfo);
375
+
376
+ if (layersInGlobalInfo) {
377
+ if (globalAlpha) writeGlobalInfoSection(writer, bitsPerChannel === 16 ? 'Mt16' : 'Mt32', !!opt.psb, () => { });
378
+ writeGlobalInfoSection(writer, bitsPerChannel === 16 ? 'Lr16' : 'Lr32', !!opt.psb, () => {
379
+ writeLayerInfoData(writer, layers, psd, globalAlpha, opt);
380
+ });
381
+ }
382
+
327
383
  writeAdditionalLayerInfo(writer, psd, psd, opt);
328
384
  }, undefined, !!opt.psb);
329
385
 
330
386
  writer.tempBuffer = undefined;
331
387
 
332
388
  // image data
333
- writeUint16(writer, Compression.RleCompressed); // Photoshop doesn't support zip compression of composite image data
389
+ if (colorMode === ColorMode.RGB && bitsPerChannel === 8 && (!imageData || is8bit(imageData))) {
390
+ writeUint16(writer, Compression.RleCompressed); // Photoshop doesn't support zip compression of composite image data
391
+
392
+ const channels = globalAlpha ? [0, 1, 2, 3] : [0, 1, 2];
393
+ const data = getCompositeData(imageData, psd.width, psd.height, globalAlpha);
334
394
 
335
- const channels = globalAlpha ? [0, 1, 2, 3] : [0, 1, 2];
336
- const data = getCompositeData(imageData, psd.width, psd.height, globalAlpha);
395
+ // encode directly into the writer buffer
396
+ const maxSize = channels.length * getRLEChannelBound(data.width, data.height, !!options.psb);
397
+ ensureSize(writer, writer.offset + maxSize);
398
+ const buffer = new Uint8Array(writer.buffer, writer.offset, maxSize);
399
+ writer.offset += writeDataRLE(buffer, data, channels, !!options.psb).length;
400
+ } else {
401
+ writeCompositeImageData(writer, psd, imageData, colorMode, bitsPerChannel, globalAlpha, !!options.psb);
402
+ }
403
+ }
337
404
 
338
- // encode directly into the writer buffer
339
- const maxSize = channels.length * getRLEChannelBound(data.width, data.height, !!options.psb);
340
- ensureSize(writer, writer.offset + maxSize);
341
- const buffer = new Uint8Array(writer.buffer, writer.offset, maxSize);
342
- writer.offset += writeDataRLE(buffer, data, channels, !!options.psb).length;
405
+ function writeGlobalInfoSection(writer: PsdWriter, key: string, psb: boolean, func: () => void) {
406
+ writeSignature(writer, psb ? '8B64' : '8BIM');
407
+ writeSignature(writer, key);
408
+ writeSection(writer, 4, func, false, psb);
409
+ }
410
+
411
+ function is8bit(data: PixelData) {
412
+ return (data.data as any).BYTES_PER_ELEMENT === 1;
413
+ }
414
+
415
+ // converts image data (8, 16 or 32 bit RGBA data) to channels of the document color mode and bit depth
416
+ function getPlanarChannels(imageData: PixelData, colorMode: ColorMode, bitsPerChannel: number, matte: boolean) {
417
+ const { data, width, height } = imageData;
418
+ const size = width * height;
419
+ const srcFloat = data instanceof Float32Array;
420
+ const srcMax = srcFloat ? 1 : (data instanceof Uint16Array ? 0xffff : 0xff);
421
+ const dstFloat = bitsPerChannel === 32;
422
+ const dstMax = dstFloat ? 1 : (bitsPerChannel === 16 ? 0xffff : 0xff);
423
+ const colorCount = colorChannelsCount(colorMode);
424
+ const create = () => dstFloat ? new Float32Array(size) : (bitsPerChannel === 16 ? new Uint16Array(size) : new Uint8Array(size));
425
+ const colors: PlanarData[] = [];
426
+ for (let i = 0; i < colorCount; i++) colors.push(create());
427
+ const alpha = create();
428
+
429
+ // color values are gamma corrected when converting between float (linear) and integer values (same as imageDataToCanvas)
430
+ const toNormalized = (v: number) => srcFloat ? Math.pow(clamp(v, 0, 1), 1 / 2.2) : v / srcMax;
431
+ const fromNormalized = (v: number) => dstFloat ? Math.pow(v, 2.2) : Math.round(clamp(v, 0, 1) * dstMax);
432
+ const sameType = srcFloat === dstFloat && srcMax === dstMax;
433
+ const convertColor = (v: number) => sameType ? (dstFloat ? v : Math.round(v)) : fromNormalized(toNormalized(v));
434
+ const convertAlpha = (v: number) => sameType ? v : (dstFloat ? v / srcMax : Math.round(clamp(v / srcMax, 0, 1) * dstMax));
435
+ const cmyk = [0, 0, 0, 0];
436
+
437
+ for (let i = 0, p = 0; i < size; i++, p += 4) {
438
+ const a = data[p + 3];
439
+ const alphaValue = convertAlpha(a);
440
+ alpha[i] = alphaValue;
441
+ // composite image colors are blended with white (or no ink) for documents with transparency
442
+ const blend = matte && a !== srcMax && a > 0 ? clamp(a / srcMax, 0, 1) : 1;
443
+
444
+ if (colorMode === ColorMode.CMYK) {
445
+ rgbToCmykValues(toNormalized(data[p]), toNormalized(data[p + 1]), toNormalized(data[p + 2]), cmyk);
446
+
447
+ for (let j = 0; j < 4; j++) {
448
+ colors[j][i] = Math.round((1 - clamp(cmyk[j] * blend, 0, 1)) * dstMax); // stored inverted (0 = 100% ink)
449
+ }
450
+ } else {
451
+ for (let j = 0; j < colorCount; j++) {
452
+ let value: number;
453
+
454
+ if (colorMode === ColorMode.RGB) {
455
+ value = convertColor(data[p + j]);
456
+ } else {
457
+ const r = data[p], g = data[p + 1], b = data[p + 2];
458
+ value = convertColor((r === g && g === b) ? r : (0.299 * r + 0.587 * g + 0.114 * b));
459
+ }
460
+
461
+ if (blend !== 1) value = dstFloat ? value * blend + 1 - blend : Math.round(value * blend + dstMax * (1 - blend));
462
+ colors[j][i] = value;
463
+ }
464
+ }
465
+ }
466
+
467
+ return { colors, alpha };
468
+ }
469
+
470
+ function encodeChannel(values: PlanarData, width: number, height: number, options: WriteOptions) {
471
+ if (!(values instanceof Uint8Array)) {
472
+ return { data: writeDataZipWithPrediction(values, width, height), compression: Compression.ZipWithPrediction };
473
+ } else if (options.compress) {
474
+ return { data: deflateSync(values), compression: Compression.ZipWithoutPrediction };
475
+ } else {
476
+ return { data: writeDataRLEBytes(values, width, height, !!options.psb), compression: Compression.RleCompressed };
477
+ }
478
+ }
479
+
480
+ function writeCompositeImageData(
481
+ writer: PsdWriter, psd: Psd, imageData: PixelData | undefined, colorMode: ColorMode, bitsPerChannel: number, globalAlpha: boolean, psb: boolean
482
+ ) {
483
+ const { width, height } = psd;
484
+ const data = imageData ?? { data: new Uint8Array(width * height * 4), width, height };
485
+ const parts: Uint8Array[] = [];
486
+
487
+ if (colorMode === ColorMode.Bitmap || colorMode === ColorMode.Indexed) {
488
+ const values = colorMode === ColorMode.Bitmap ?
489
+ getBitmapData(data) :
490
+ getIndexedData(data, psd.palette!, psd.imageResources?.transparencyIndex);
491
+ const rowBytes = colorMode === ColorMode.Bitmap ? Math.ceil(width / 8) : width;
492
+
493
+ if (colorMode === ColorMode.Bitmap) {
494
+ writeUint16(writer, Compression.RawData);
495
+ parts.push(values);
496
+ } else {
497
+ writeUint16(writer, Compression.RleCompressed);
498
+ parts.push(writeDataRLEBytes(values, rowBytes, height, psb));
499
+ }
500
+ } else {
501
+ const { colors, alpha } = getPlanarChannels(data, colorMode, bitsPerChannel, globalAlpha);
502
+ const channels = globalAlpha ? [...colors, alpha] : colors;
503
+
504
+ if (bitsPerChannel === 8) {
505
+ // row byte counts for all channels are stored before data of all channels
506
+ const encoded = channels.map(c => writeDataRLEBytes(c as Uint8Array, width, height, psb));
507
+ const countsSize = height * (psb ? 4 : 2);
508
+ writeUint16(writer, Compression.RleCompressed);
509
+ for (const e of encoded) parts.push(e.subarray(0, countsSize));
510
+ for (const e of encoded) parts.push(e.subarray(countsSize));
511
+ } else {
512
+ writeUint16(writer, Compression.RawData); // Photoshop writes 16 and 32 bit composite image without compression
513
+ for (const c of channels) parts.push(planarToBytes(c));
514
+ }
515
+ }
516
+
517
+ for (const part of parts) writeBytes(writer, part);
518
+ }
519
+
520
+ function getBitmapData({ data, width, height }: PixelData) {
521
+ const rowBytes = Math.ceil(width / 8);
522
+ const result = new Uint8Array(rowBytes * height);
523
+ const max = data instanceof Float32Array ? 1 : (data instanceof Uint16Array ? 0xffff : 0xff);
524
+
525
+ for (let y = 0, p = 0; y < height; y++) {
526
+ for (let x = 0; x < width; x++, p += 4) {
527
+ const a = data[p + 3] / max;
528
+ const value = ((0.299 * data[p] + 0.587 * data[p + 1] + 0.114 * data[p + 2]) / max) * a + (1 - a); // blend with white
529
+ if (value < 0.5) result[y * rowBytes + (x >> 3)] |= 0x80 >> (x & 7); // 1 = black
530
+ }
531
+ }
532
+
533
+ return result;
534
+ }
535
+
536
+ function getIndexedData({ data, width, height }: PixelData, palette: { r: number; g: number; b: number; }[], transparencyIndex: number | undefined) {
537
+ const size = width * height;
538
+ const result = new Uint8Array(size);
539
+ const shift = data instanceof Uint16Array ? 8 : 0;
540
+ const cache = new Map<number, number>();
541
+
542
+ for (let i = 0; i < palette.length && i < 256; i++) {
543
+ const key = (palette[i].r << 16) | (palette[i].g << 8) | palette[i].b;
544
+ if (i !== transparencyIndex && !cache.has(key)) cache.set(key, i);
545
+ }
546
+
547
+ for (let i = 0, p = 0; i < size; i++, p += 4) {
548
+ const read = (v: number) => data instanceof Float32Array ? Math.round(Math.pow(clamp(v, 0, 1), 1 / 2.2) * 255) : (v >>> shift);
549
+ const a = data instanceof Float32Array ? data[p + 3] * 255 : (data[p + 3] >>> shift);
550
+
551
+ if (transparencyIndex !== undefined && transparencyIndex >= 0 && a < 128) {
552
+ result[i] = transparencyIndex;
553
+ continue;
554
+ }
555
+
556
+ const r = read(data[p]), g = read(data[p + 1]), b = read(data[p + 2]);
557
+ const key = (r << 16) | (g << 8) | b;
558
+ let index = cache.get(key);
559
+
560
+ if (index === undefined) {
561
+ // use closest color from the palette
562
+ let best = Infinity;
563
+ index = 0;
564
+
565
+ for (let j = 0; j < palette.length && j < 256; j++) {
566
+ if (j === transparencyIndex) continue;
567
+ const dr = palette[j].r - r, dg = palette[j].g - g, db = palette[j].b - b;
568
+ const distance = dr * dr + dg * dg + db * db;
569
+ if (distance < best) { best = distance; index = j; }
570
+ }
571
+
572
+ cache.set(key, index);
573
+ }
574
+
575
+ result[i] = index;
576
+ }
577
+
578
+ return result;
343
579
  }
344
580
 
345
581
  function getCompositeData(imageData: PixelData | undefined, width: number, height: number, globalAlpha: boolean): PixelData {
@@ -368,64 +604,66 @@ function getCompositeData(imageData: PixelData | undefined, width: number, heigh
368
604
  }
369
605
 
370
606
  function writeLayerInfo(writer: PsdWriter, layers: Layer[], psd: Psd, globalAlpha: boolean, options: ExtendedWriteOptions) {
371
- writeSection(writer, 4, () => {
372
- writeInt16(writer, globalAlpha ? -layers.length : layers.length);
607
+ writeSection(writer, 4, () => writeLayerInfoData(writer, layers, psd, globalAlpha, options), true, options.psb);
608
+ }
373
609
 
374
- const layersData = layers.map((l, i) => getChannels(writer, l, i === 0, options));
610
+ function writeLayerInfoData(writer: PsdWriter, layers: Layer[], psd: Psd, globalAlpha: boolean, options: ExtendedWriteOptions) {
611
+ writeInt16(writer, globalAlpha ? -layers.length : layers.length);
375
612
 
376
- // layer records
377
- for (const layerData of layersData) {
378
- const { layer, top, left, bottom, right, channels } = layerData;
613
+ const layersData = layers.map((l, i) => getChannels(writer, l, i === 0, options));
379
614
 
380
- writeInt32(writer, top);
381
- writeInt32(writer, left);
382
- writeInt32(writer, bottom);
383
- writeInt32(writer, right);
384
- writeUint16(writer, channels.length);
615
+ // layer records
616
+ for (const layerData of layersData) {
617
+ const { layer, top, left, bottom, right, channels } = layerData;
385
618
 
386
- for (const c of channels) {
387
- writeInt16(writer, c.id);
388
- if (options.psb) {
389
- writeUint64(writer, c.length);
390
- } else {
391
- writeUint32(writer, c.length);
392
- }
619
+ writeInt32(writer, top);
620
+ writeInt32(writer, left);
621
+ writeInt32(writer, bottom);
622
+ writeInt32(writer, right);
623
+ writeUint16(writer, channels.length);
624
+
625
+ for (const c of channels) {
626
+ writeInt16(writer, c.id);
627
+ if (options.psb) {
628
+ writeUint64(writer, c.length);
629
+ } else {
630
+ writeUint32(writer, c.length);
393
631
  }
632
+ }
394
633
 
395
- writeSignature(writer, '8BIM');
396
- writeSignature(writer, fromBlendMode[layer.blendMode!] || 'norm');
397
- writeUint8(writer, Math.round(clamp(layer.opacity ?? 1, 0, 1) * 255));
398
- writeUint8(writer, layer.clipping ? 1 : 0);
634
+ writeSignature(writer, '8BIM');
635
+ writeSignature(writer, fromBlendMode[layer.blendMode!] || 'norm');
636
+ writeUint8(writer, Math.round(clamp(layer.opacity ?? 1, 0, 1) * 255));
637
+ writeUint8(writer, layer.clipping ? 1 : 0);
399
638
 
400
- let flags = 0x08; // 1 for Photoshop 5.0 and later, tells if bit 4 has useful information
401
- if (layer.transparencyProtected) flags |= 0x01;
402
- if (layer.hidden) flags |= 0x02;
403
- if (layer.vectorMask || (layer.sectionDivider && layer.sectionDivider.type !== SectionDividerType.Other) || layer.adjustment) {
404
- flags |= 0x10; // pixel data irrelevant to appearance of document
405
- }
406
- if (layer.effectsOpen) flags |= 0x20;
407
-
408
- writeUint8(writer, flags);
409
- writeUint8(writer, 0); // filler
410
- writeSection(writer, 1, () => {
411
- writeLayerMaskData(writer, layer, layerData);
412
- writeLayerBlendingRanges(writer, layer);
413
- writePascalString(writer, (layer.name || '').substring(0, 255), 4);
414
- writeAdditionalLayerInfo(writer, layer, psd, options);
415
- });
639
+ let flags = 0x08; // 1 for Photoshop 5.0 and later, tells if bit 4 has useful information
640
+ if (layer.transparencyProtected) flags |= 0x01;
641
+ if (layer.hidden) flags |= 0x02;
642
+ if (layer.vectorMask || (layer.sectionDivider && layer.sectionDivider.type !== SectionDividerType.Other) || layer.adjustment) {
643
+ flags |= 0x10; // pixel data irrelevant to appearance of document
416
644
  }
645
+ if (layer.effectsOpen) flags |= 0x20;
646
+
647
+ writeUint8(writer, flags);
648
+ writeUint8(writer, 0); // filler
649
+ writeSection(writer, 1, () => {
650
+ writeLayerMaskData(writer, layer, layerData);
651
+ writeLayerBlendingRanges(writer, layer);
652
+ writePascalString(writer, (layer.name || '').substring(0, 255), 4);
653
+ writeAdditionalLayerInfo(writer, layer, psd, options);
654
+ });
655
+ }
417
656
 
418
- // layer channel image data
419
- for (const layerData of layersData) {
420
- for (const channel of layerData.channels) {
421
- writeUint16(writer, channel.compression);
657
+ // layer channel image data
658
+ for (const layerData of layersData) {
659
+ for (const channel of layerData.channels) {
660
+ writeUint16(writer, channel.compression);
422
661
 
423
- if (channel.data) {
424
- writeBytes(writer, channel.data);
425
- }
662
+ if (channel.data) {
663
+ writeBytes(writer, channel.data);
426
664
  }
427
665
  }
428
- }, true, options.psb);
666
+ }
429
667
  }
430
668
 
431
669
  // real mask is only stored separately if layer has both vector and pixel masks, otherwise it's stored as the main mask
@@ -661,10 +899,14 @@ function getMaskChannels(tempBuffer: Uint8Array, layerData: LayerChannelData, ma
661
899
 
662
900
  let buffer: Uint8Array;
663
901
  let compression: Compression;
902
+ const bitsPerChannel = (options as ExtendedWriteOptions).bitsPerChannel ?? 8;
664
903
 
665
904
  if (!imageData) {
666
905
  buffer = new Uint8Array(0);
667
906
  compression = Compression.RleCompressed;
907
+ } else if (bitsPerChannel !== 8 || !is8bit(imageData)) {
908
+ const { colors } = getPlanarChannels(imageData, ColorMode.RGB, bitsPerChannel, false);
909
+ ({ data: buffer, compression } = encodeChannel(colors[0], width, height, options));
668
910
  } else if (options.compress) {
669
911
  buffer = writeDataZipWithoutPrediction(imageData, [0]);
670
912
  compression = Compression.ZipWithoutPrediction;
@@ -692,8 +934,9 @@ function getChannels(writer: PsdWriter, layer: Layer, background: boolean, optio
692
934
 
693
935
  if (layer.rawData) {
694
936
  const { colorMode, bitsPerChannel, large } = layer.rawData;
937
+ const opt = options as ExtendedWriteOptions;
695
938
 
696
- if (large === !!options.psb && colorMode === ColorMode.RGB && bitsPerChannel === 8) {
939
+ if (large === !!options.psb && colorMode === (opt.colorMode ?? ColorMode.RGB) && bitsPerChannel === (opt.bitsPerChannel ?? 8)) {
697
940
  // same mask mapping as in writeLayerMaskData
698
941
  const { mask, realMask } = getMasks(layer);
699
942
  return {
@@ -705,8 +948,6 @@ function getChannels(writer: PsdWriter, layer: Layer, background: boolean, optio
705
948
  };
706
949
  }
707
950
 
708
- if (bitsPerChannel !== 8) throw new Error('Layer raw data with bitsPerChannel other than 8 is not supported for writing');
709
-
710
951
  // raw data is in different format than the one we're writing, decode it so it can be re-encoded
711
952
  source = {
712
953
  ...layer,
@@ -739,11 +980,8 @@ function getLayerDimentions({ canvas, imageData }: Layer): { width: number; heig
739
980
  }
740
981
 
741
982
  function cropImageData(data: PixelData, left: number, top: number, width: number, height: number) {
742
- if (data.data instanceof Uint32Array || data.data instanceof Uint16Array) {
743
- throw new Error('imageData has incorrect bit depth');
744
- }
745
-
746
- const croppedData = createImageData(width, height);
983
+ const croppedData: PixelData = is8bit(data) ? createImageData(width, height) :
984
+ { width, height, data: new (data.data.constructor as any)(width * height * 4) };
747
985
  const srcData = data.data;
748
986
  const dstData = croppedData.data;
749
987
 
@@ -775,8 +1013,9 @@ function getLayerChannels(tempBuffer: Uint8Array, layer: Layer, background: bool
775
1013
  let { width, height } = getLayerDimentions(layer);
776
1014
 
777
1015
  if (!(layer.canvas || layer.imageData) || !width || !height) {
778
- right = left;
779
- bottom = top;
1016
+ // layer without image data is empty, keep inverted (negative size) bounds from some files as they are
1017
+ if (layer.right === undefined || right > left) right = left;
1018
+ if (layer.bottom === undefined || bottom > top) bottom = top;
780
1019
  return { layer, top, left, right, bottom, channels };
781
1020
  }
782
1021
 
@@ -812,6 +1051,22 @@ function getLayerChannels(tempBuffer: Uint8Array, layer: Layer, background: bool
812
1051
  channelIds.unshift(ChannelID.Transparency);
813
1052
  }
814
1053
 
1054
+ const colorMode = (options as ExtendedWriteOptions).colorMode ?? ColorMode.RGB;
1055
+ const bitsPerChannel = (options as ExtendedWriteOptions).bitsPerChannel ?? 8;
1056
+
1057
+ if (colorMode !== ColorMode.RGB || bitsPerChannel !== 8 || !is8bit(imageData)) {
1058
+ const { colors, alpha } = getPlanarChannels(imageData, colorMode, bitsPerChannel, false);
1059
+ const ids = channelIds[0] === ChannelID.Transparency ? [ChannelID.Transparency] : [];
1060
+ for (let i = 0; i < colors.length; i++) ids.push(i as ChannelID);
1061
+
1062
+ channels = ids.map(id => {
1063
+ const { data, compression } = encodeChannel(id === ChannelID.Transparency ? alpha : colors[id], width, height, options);
1064
+ return { id, compression, data, length: 2 + data.length };
1065
+ });
1066
+
1067
+ return { layer, top, left, right, bottom, channels };
1068
+ }
1069
+
815
1070
  channels = channelIds.map(id => {
816
1071
  const offset = offsetForChannel(id, false); // image data is always RGBA
817
1072
  let data: Uint8Array;
package/src/text.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TextStyle, LayerTextData, ParagraphStyle, Font, AntiAlias, TextGridInfo, Justification, Color, TextPath } from './psd';
1
+ import type { TextStyle, LayerTextData, ParagraphStyle, Font, AntiAlias, TextGridInfo, Justification, Color, TextPath, TextStyleRun } from './psd';
2
2
  import { clamp } from './helpers';
3
3
 
4
4
  interface Adjustments {
@@ -633,6 +633,81 @@ export function decodeEngineData(engineData: EngineData) {
633
633
  return result;
634
634
  }
635
635
 
636
+ // type tool info from Photoshop 5.0 and 5.5 ('tySh')
637
+ export interface LegacyTextData {
638
+ transform: number[];
639
+ faces: { mark: number; name: string; familyName: string; styleName: string; script: number; }[];
640
+ styles: {
641
+ mark: number; faceMark: number; size: number; tracking: number; kerning: number; leading: number;
642
+ baselineShift: number; autoKerning: boolean;
643
+ }[];
644
+ lines: { orientation: number; alignment: number; chars: { code: number; style: number; }[]; }[];
645
+ color: Color;
646
+ antiAlias: boolean;
647
+ }
648
+
649
+ const fixed = (value: number) => value / 0x10000; // 16.16 fixed point value
650
+
651
+ export function decodeLegacyTextData(data: LegacyTextData): LayerTextData {
652
+ const styleRuns: { length: number; mark: number; }[] = [];
653
+ let text = '';
654
+
655
+ for (const line of data.lines) {
656
+ for (const { code, style } of line.chars) {
657
+ text += String.fromCharCode(code);
658
+ const last = styleRuns[styleRuns.length - 1];
659
+
660
+ if (last && last.mark === style) {
661
+ last.length++;
662
+ } else {
663
+ styleRuns.push({ length: 1, mark: style });
664
+ }
665
+ }
666
+ }
667
+
668
+ // remove one trailing new line (Photoshop always appends it)
669
+ if (text.endsWith('\r')) {
670
+ text = text.slice(0, -1);
671
+ if (--styleRuns[styleRuns.length - 1].length === 0) styleRuns.pop();
672
+ }
673
+
674
+ const runs: TextStyleRun[] = styleRuns.map(({ length, mark }) => {
675
+ const s = data.styles.find(s => s.mark === mark) ?? data.styles[0];
676
+ const face = s && (data.faces.find(f => f.mark === s.faceMark) ?? data.faces[0]);
677
+ const style: TextStyle = {};
678
+
679
+ if (face) style.font = { name: face.name, script: face.script };
680
+
681
+ if (s) {
682
+ style.fontSize = fixed(s.size);
683
+ style.autoLeading = s.leading === 0;
684
+ if (s.leading) style.leading = fixed(s.leading);
685
+ style.tracking = Math.round(fixed(s.tracking) * 1000); // stored in ems
686
+ style.autoKerning = s.autoKerning;
687
+ style.kerning = Math.round(fixed(s.kerning) * 1000); // stored in ems
688
+ style.baselineShift = fixed(s.baselineShift);
689
+ }
690
+
691
+ style.fillColor = data.color;
692
+ return { length, style };
693
+ });
694
+
695
+ const result: LayerTextData = {
696
+ text: text.replace(/\r/g, '\n'),
697
+ transform: data.transform,
698
+ antiAlias: data.antiAlias ? 'smooth' : 'none',
699
+ orientation: data.lines[0]?.orientation === 1 ? 'vertical' : 'horizontal',
700
+ paragraphStyle: { justification: justification[data.lines[0]?.alignment ?? 0] ?? 'left' },
701
+ style: {},
702
+ styleRuns: runs,
703
+ };
704
+
705
+ deduplicateValues(result.style!, result.styleRuns!, styleKeys);
706
+ if (!result.styleRuns!.length) delete result.styleRuns;
707
+
708
+ return result;
709
+ }
710
+
636
711
  // Adjusts run lengths (specified in terms of the original text) for characters removed from the text
637
712
  // (\r characters of \r\n pairs that are collapsed into single \r)
638
713
  function adjustRunLengths<T extends { length: number; }>(runs: T[] | undefined, removed: number[]): T[] | undefined {