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.
- package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/4511bacf +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/68416cb2 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/71df9cb3 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/acc36e66 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/b3c2fab7 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/c0b789d2 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/c314aece +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/cfc49f4f +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/d13d7610 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/dc86f000 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/e03e2acd +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/README_PSD.md +25 -9
- package/TODO.md +13 -84
- package/dist/abr.js +12 -7
- package/dist/abr.js.map +1 -1
- package/dist/additionalInfo.d.ts +2 -0
- package/dist/additionalInfo.js +361 -75
- package/dist/additionalInfo.js.map +1 -1
- package/dist/bundle.js +1524 -389
- package/dist/descriptor.d.ts +14 -5
- package/dist/descriptor.js +102 -64
- package/dist/descriptor.js.map +1 -1
- package/dist/helpers.d.ts +6 -0
- package/dist/helpers.js +242 -2
- package/dist/helpers.js.map +1 -1
- package/dist/imageResources.js +84 -3
- package/dist/imageResources.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/psd.d.ts +144 -2
- package/dist/psd.js.map +1 -1
- package/dist/psdReader.js +206 -110
- package/dist/psdReader.js.map +1 -1
- package/dist/psdWriter.d.ts +1 -0
- package/dist/psdWriter.js +336 -91
- package/dist/psdWriter.js.map +1 -1
- package/dist/text.d.ts +32 -1
- package/dist/text.js +58 -0
- package/dist/text.js.map +1 -1
- package/dist-es/abr.js +12 -7
- package/dist-es/abr.js.map +1 -1
- package/dist-es/additionalInfo.d.ts +2 -0
- package/dist-es/additionalInfo.js +361 -75
- package/dist-es/additionalInfo.js.map +1 -1
- package/dist-es/descriptor.d.ts +14 -5
- package/dist-es/descriptor.js +102 -64
- package/dist-es/descriptor.js.map +1 -1
- package/dist-es/helpers.d.ts +6 -0
- package/dist-es/helpers.js +242 -2
- package/dist-es/helpers.js.map +1 -1
- package/dist-es/imageResources.js +84 -3
- package/dist-es/imageResources.js.map +1 -1
- package/dist-es/index.d.ts +1 -0
- package/dist-es/index.js +1 -0
- package/dist-es/index.js.map +1 -1
- package/dist-es/psd.d.ts +144 -2
- package/dist-es/psd.js.map +1 -1
- package/dist-es/psdReader.js +206 -110
- package/dist-es/psdReader.js.map +1 -1
- package/dist-es/psdWriter.d.ts +1 -0
- package/dist-es/psdWriter.js +336 -91
- package/dist-es/psdWriter.js.map +1 -1
- package/dist-es/text.d.ts +32 -1
- package/dist-es/text.js +58 -0
- package/dist-es/text.js.map +1 -1
- package/package.json +1 -1
- package/perf.js +312 -9
- package/src/abr.ts +14 -7
- package/src/additionalInfo.ts +433 -87
- package/src/descriptor.ts +103 -58
- package/src/helpers.ts +262 -2
- package/src/imageResources.ts +107 -4
- package/src/index.ts +1 -0
- package/src/psd.ts +115 -2
- package/src/psdReader.ts +226 -108
- package/src/psdWriter.ts +345 -90
- 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
|
|
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
|
|
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 (
|
|
244
|
-
throw new Error('
|
|
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
|
-
|
|
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 ?
|
|
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,
|
|
332
|
+
writeUint16(writer, colorMode);
|
|
299
333
|
|
|
300
|
-
// color mode data
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
607
|
+
writeSection(writer, 4, () => writeLayerInfoData(writer, layers, psd, globalAlpha, options), true, options.psb);
|
|
608
|
+
}
|
|
373
609
|
|
|
374
|
-
|
|
610
|
+
function writeLayerInfoData(writer: PsdWriter, layers: Layer[], psd: Psd, globalAlpha: boolean, options: ExtendedWriteOptions) {
|
|
611
|
+
writeInt16(writer, globalAlpha ? -layers.length : layers.length);
|
|
375
612
|
|
|
376
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
662
|
+
if (channel.data) {
|
|
663
|
+
writeBytes(writer, channel.data);
|
|
426
664
|
}
|
|
427
665
|
}
|
|
428
|
-
}
|
|
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
|
-
|
|
743
|
-
|
|
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
|
-
|
|
779
|
-
|
|
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 {
|