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/psdReader.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Inflate } from 'pako';
|
|
2
2
|
import { Psd, Layer, ColorMode, SectionDividerType, LayerAdditionalInfo, ReadOptions, LayerMaskData, Color, PatternInfo, GlobalLayerMaskInfo, RGB, PixelData, PixelArray, ChannelID, Compression } from './psd';
|
|
3
|
-
import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, LayerMaskFlags, MaskParams, ColorSpace, largeAdditionalInfoKeys, largeAdditionalInfoKeysObserved, imageDataToCanvas, normalizeLabAB } from './helpers';
|
|
3
|
+
import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, LayerMaskFlags, MaskParams, ColorSpace, largeAdditionalInfoKeys, largeAdditionalInfoKeysObserved, imageDataToCanvas, normalizeLabAB, cmykToRgbValues } from './helpers';
|
|
4
4
|
import { infoHandlersMap } from './additionalInfo';
|
|
5
5
|
import { decodeString } from './utf8';
|
|
6
6
|
import { InternalImageResources, resourceHandlersMap } from './imageResources';
|
|
@@ -185,6 +185,8 @@ export function readUnicodeString(reader: PsdReader) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
const STRING_CHUNK_SIZE = 0x2000;
|
|
188
|
+
// shorter strings (most descriptor keys, OSTypes and names) are faster to build with simple loop than String.fromCharCode.apply
|
|
189
|
+
const SHORT_STRING_LENGTH = 64;
|
|
188
190
|
|
|
189
191
|
function checkReadBounds(reader: PsdReader, size: number) {
|
|
190
192
|
if (reader.offset < 0 || (reader.offset + size) > reader.view.byteLength) {
|
|
@@ -198,10 +200,21 @@ function readUtf16String(reader: PsdReader, length: number, littleEndian: boolea
|
|
|
198
200
|
checkReadBounds(reader, length * 2);
|
|
199
201
|
|
|
200
202
|
const view = reader.view;
|
|
201
|
-
const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
|
|
202
203
|
let offset = reader.offset;
|
|
203
204
|
let text = '';
|
|
204
205
|
|
|
206
|
+
if (length <= SHORT_STRING_LENGTH) {
|
|
207
|
+
for (let i = 0; i < length; i++, offset += 2) {
|
|
208
|
+
text += String.fromCharCode(view.getUint16(offset, littleEndian));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
reader.offset = offset;
|
|
212
|
+
if (text.charCodeAt(text.length - 1) === 0) text = text.substring(0, text.length - 1);
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
|
|
217
|
+
|
|
205
218
|
for (let i = 0; i < length; i += STRING_CHUNK_SIZE) {
|
|
206
219
|
const count = Math.min(STRING_CHUNK_SIZE, length - i);
|
|
207
220
|
|
|
@@ -241,6 +254,19 @@ export function readUnicodeStringWithLengthLE(reader: PsdReader, length: number)
|
|
|
241
254
|
export function readAsciiString(reader: PsdReader, length: number) {
|
|
242
255
|
if (!(length > 0)) return '';
|
|
243
256
|
checkReadBounds(reader, length);
|
|
257
|
+
|
|
258
|
+
if (length <= SHORT_STRING_LENGTH) {
|
|
259
|
+
const view = reader.view;
|
|
260
|
+
let text = '';
|
|
261
|
+
|
|
262
|
+
for (let i = 0, offset = reader.offset; i < length; i++, offset++) {
|
|
263
|
+
text += String.fromCharCode(view.getUint8(offset));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
reader.offset += length;
|
|
267
|
+
return text;
|
|
268
|
+
}
|
|
269
|
+
|
|
244
270
|
const text = bytesToString(new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length));
|
|
245
271
|
reader.offset += length;
|
|
246
272
|
return text;
|
|
@@ -260,7 +286,16 @@ export function checkSignature(reader: PsdReader, a: string, b?: string) {
|
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
function readShortString(reader: PsdReader, length: number) {
|
|
263
|
-
|
|
289
|
+
const bytes = readBytes(reader, length);
|
|
290
|
+
if (length > SHORT_STRING_LENGTH) return bytesToString(bytes);
|
|
291
|
+
|
|
292
|
+
let text = '';
|
|
293
|
+
|
|
294
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
295
|
+
text += String.fromCharCode(bytes[i]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return text;
|
|
264
299
|
}
|
|
265
300
|
|
|
266
301
|
function isValidSignature(sig: string) {
|
|
@@ -458,6 +493,10 @@ export function readLayerInfo(reader: PsdReader, psd: Psd, imageResources: Inter
|
|
|
458
493
|
l.opened = type === SectionDividerType.OpenFolder;
|
|
459
494
|
l.children = [];
|
|
460
495
|
|
|
496
|
+
// group layers can't have image data, some broken files have it, ignore it like Photoshop does
|
|
497
|
+
delete l.canvas;
|
|
498
|
+
delete l.imageData;
|
|
499
|
+
|
|
461
500
|
if (l.sectionDivider?.key) {
|
|
462
501
|
l.blendMode = toBlendMode[l.sectionDivider.key] ?? l.blendMode;
|
|
463
502
|
}
|
|
@@ -675,8 +714,8 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
|
|
|
675
714
|
let compression = Compression.RawData;
|
|
676
715
|
let data: Uint8Array | undefined = undefined;
|
|
677
716
|
|
|
678
|
-
|
|
679
|
-
if (channel.length) {
|
|
717
|
+
// channel length of 1 is invalid (should be 0 or at least 2), skip the byte and treat channel as empty
|
|
718
|
+
if (channel.length >= 2) {
|
|
680
719
|
compression = readUint16(reader) as Compression;
|
|
681
720
|
// try to fix broken files where there's 1 byte shift of channel
|
|
682
721
|
if (compression > 3) {
|
|
@@ -702,7 +741,14 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
|
|
|
702
741
|
}
|
|
703
742
|
|
|
704
743
|
if (!reader.useRawData) {
|
|
705
|
-
|
|
744
|
+
try {
|
|
745
|
+
decodeLayerImageData(layer, reader);
|
|
746
|
+
} catch (e) {
|
|
747
|
+
// corrupted image data of one layer shouldn't prevent reading rest of the file
|
|
748
|
+
if (reader.throwForMissingFeatures || (e as Error).message === 'Exceeded memory limit') throw e;
|
|
749
|
+
reader.logMissingFeatures && reader.log(`Failed to decode layer image data: ${(e as Error).message}`);
|
|
750
|
+
delete layer.rawData;
|
|
751
|
+
}
|
|
706
752
|
}
|
|
707
753
|
}
|
|
708
754
|
|
|
@@ -924,7 +970,7 @@ export function readGlobalLayerMaskInfo(reader: PsdReader) {
|
|
|
924
970
|
const colorSpace2 = readUint16(reader);
|
|
925
971
|
const colorSpace3 = readUint16(reader);
|
|
926
972
|
const colorSpace4 = readUint16(reader);
|
|
927
|
-
const opacity = readUint16(reader) / 100; // 0 - 100
|
|
973
|
+
const opacity = Math.min(readUint16(reader), 100) / 100; // 0 - 100 (some files have values out of range)
|
|
928
974
|
const kind = readUint8(reader);
|
|
929
975
|
skipBytes(reader, left()); // 3 bytes of padding ?
|
|
930
976
|
return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind };
|
|
@@ -1191,10 +1237,7 @@ function toRange(value: number, max: number) {
|
|
|
1191
1237
|
return value <= 0 ? 0 : (value >= max ? max : value);
|
|
1192
1238
|
}
|
|
1193
1239
|
|
|
1194
|
-
//
|
|
1195
|
-
// Coefficients are taken from pdf.js (DeviceCmykCS in src/core/colorspace.js, https://github.com/mozilla/pdf.js),
|
|
1196
|
-
// Copyright 2012 Mozilla Foundation, licensed under the Apache License, Version 2.0
|
|
1197
|
-
// (http://www.apache.org/licenses/LICENSE-2.0)
|
|
1240
|
+
// converts CMYK data to RGB using approximation of U.S. Web Coated (SWOP) v2 profile (see cmykToRgbValues)
|
|
1198
1241
|
// cmyk - 8 or 16 bit data with 5 channels (cmyka), rgb - 8 or 16 bit data with 4 channels (rgba)
|
|
1199
1242
|
// removeMatte - colors are blended with white (no ink), used for composite image of documents with global alpha
|
|
1200
1243
|
function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
|
|
@@ -1203,6 +1246,7 @@ function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
|
|
|
1203
1246
|
const dstData = rgb.data;
|
|
1204
1247
|
const srcMax = srcData instanceof Uint16Array ? 0xffff : 0xff;
|
|
1205
1248
|
const dstMax = dstData instanceof Uint16Array ? 0xffff : 0xff;
|
|
1249
|
+
const out = [0, 0, 0];
|
|
1206
1250
|
|
|
1207
1251
|
for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
|
|
1208
1252
|
const a = srcData[src + 4];
|
|
@@ -1221,21 +1265,10 @@ function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
|
|
|
1221
1265
|
k = Math.min(1, k * ra);
|
|
1222
1266
|
}
|
|
1223
1267
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
k * (-21.86122147463605 * k - 189.48180835922747), dstMax);
|
|
1229
|
-
dstData[dst + 1] = toRange(255 +
|
|
1230
|
-
c * (8.841041422036149 * c + 60.118027045597366 * m + 6.871425592049007 * y + 31.159100130055922 * k - 79.2970844816548) +
|
|
1231
|
-
m * (-15.310361306967817 * m + 17.575251261109482 * y + 131.35250912493976 * k - 190.9453302588951) +
|
|
1232
|
-
y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) +
|
|
1233
|
-
k * (-20.737325471181034 * k - 187.80453709719578), dstMax);
|
|
1234
|
-
dstData[dst + 2] = toRange(255 +
|
|
1235
|
-
c * (0.8842522430003296 * c + 8.078677503112928 * m + 30.89978309703729 * y - 0.23883238689178934 * k - 14.183576799673286) +
|
|
1236
|
-
m * (10.49593273432072 * m + 63.02378494754052 * y + 50.606957656360734 * k - 112.23884253719248) +
|
|
1237
|
-
y * (0.03296041114873217 * y + 115.60384449646641 * k - 193.58209356861505) +
|
|
1238
|
-
k * (-22.33816807309886 * k - 180.12613974708367), dstMax);
|
|
1268
|
+
cmykToRgbValues(c, m, y, k, out);
|
|
1269
|
+
dstData[dst] = toRange(out[0], dstMax);
|
|
1270
|
+
dstData[dst + 1] = toRange(out[1], dstMax);
|
|
1271
|
+
dstData[dst + 2] = toRange(out[2], dstMax);
|
|
1239
1272
|
dstData[dst + 3] = srcMax === dstMax ? a : Math.round(a * dstMax / srcMax);
|
|
1240
1273
|
}
|
|
1241
1274
|
}
|
|
@@ -1418,8 +1451,8 @@ export function readDataZip(
|
|
|
1418
1451
|
}
|
|
1419
1452
|
}
|
|
1420
1453
|
|
|
1421
|
-
// decodes single PackBits row, returns number of decoded
|
|
1422
|
-
function decodeRLERow(src: Uint8Array, i: number, end: number, dst:
|
|
1454
|
+
// decodes single PackBits row into byte buffer, returns number of decoded bytes
|
|
1455
|
+
function decodeRLERow(src: Uint8Array, i: number, end: number, dst: Uint8Array, count: number) {
|
|
1423
1456
|
let x = 0;
|
|
1424
1457
|
|
|
1425
1458
|
while (i < end && x < count) {
|
|
@@ -1431,19 +1464,15 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
|
|
|
1431
1464
|
const value = src[i++];
|
|
1432
1465
|
let n = 257 - header;
|
|
1433
1466
|
if (n > count - x) n = count - x;
|
|
1467
|
+
dst.fill(value, x, x + n);
|
|
1434
1468
|
x += n;
|
|
1435
|
-
|
|
1436
|
-
for (; n > 0; n--, p += step) {
|
|
1437
|
-
dst[p] = value;
|
|
1438
|
-
}
|
|
1439
1469
|
} else if (header < 128) {
|
|
1440
1470
|
let n = header + 1;
|
|
1441
1471
|
if (n > count - x) n = count - x;
|
|
1442
1472
|
if (n > end - i) n = end - i;
|
|
1443
|
-
x += n;
|
|
1444
1473
|
|
|
1445
|
-
for (; n > 0; n
|
|
1446
|
-
dst[
|
|
1474
|
+
for (; n > 0; n--) {
|
|
1475
|
+
dst[x++] = src[i++];
|
|
1447
1476
|
}
|
|
1448
1477
|
} else {
|
|
1449
1478
|
// ignore 128
|
|
@@ -1453,6 +1482,51 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
|
|
|
1453
1482
|
return x;
|
|
1454
1483
|
}
|
|
1455
1484
|
|
|
1485
|
+
// decodes RLE rows of single 8bit channel, writing values to `data` with `step`
|
|
1486
|
+
// (kept as single function with the loops, inlining row decoder into readDataRLE makes V8 generate much slower code)
|
|
1487
|
+
function decodeRLEChannel8(
|
|
1488
|
+
src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
|
|
1489
|
+
data: PixelArray, offset: number, step: number
|
|
1490
|
+
) {
|
|
1491
|
+
const srcEnd = src.length | 0;
|
|
1492
|
+
|
|
1493
|
+
for (let y = 0; y < height; y++, li++) {
|
|
1494
|
+
let i = (pos < srcEnd ? pos : srcEnd) | 0;
|
|
1495
|
+
pos += lengths[li];
|
|
1496
|
+
const end = (pos < srcEnd ? pos : srcEnd) | 0;
|
|
1497
|
+
let p = (offset + y * width * step) | 0;
|
|
1498
|
+
let x = 0;
|
|
1499
|
+
|
|
1500
|
+
while (i < end && x < width) {
|
|
1501
|
+
const header = src[i++] | 0;
|
|
1502
|
+
|
|
1503
|
+
if (header > 128) {
|
|
1504
|
+
if (i >= end) break;
|
|
1505
|
+
|
|
1506
|
+
const value = src[i++] | 0;
|
|
1507
|
+
let n = (257 - header) | 0;
|
|
1508
|
+
if (n > width - x) n = (width - x) | 0;
|
|
1509
|
+
x = (x + n) | 0;
|
|
1510
|
+
|
|
1511
|
+
for (const pe = (p + n * step) | 0; p < pe; p = (p + step) | 0) {
|
|
1512
|
+
data[p] = value;
|
|
1513
|
+
}
|
|
1514
|
+
} else if (header < 128) {
|
|
1515
|
+
let n = (header + 1) | 0;
|
|
1516
|
+
if (n > width - x) n = (width - x) | 0;
|
|
1517
|
+
if (n > end - i) n = (end - i) | 0;
|
|
1518
|
+
x = (x + n) | 0;
|
|
1519
|
+
|
|
1520
|
+
for (const ie = (i + n) | 0; i < ie; i = (i + 1) | 0, p = (p + step) | 0) {
|
|
1521
|
+
data[p] = src[i];
|
|
1522
|
+
}
|
|
1523
|
+
} else {
|
|
1524
|
+
// ignore 128
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1456
1530
|
// converts big-endian 16/32-bit values from bytes and writes them to `data` starting at `p` with `step`
|
|
1457
1531
|
function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: PixelArray, p: number, step: number) {
|
|
1458
1532
|
if (bpv === 2) {
|
|
@@ -1468,10 +1542,10 @@ function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: Pixe
|
|
|
1468
1542
|
}
|
|
1469
1543
|
}
|
|
1470
1544
|
|
|
1471
|
-
// decodes RLE rows of single channel, each row
|
|
1472
|
-
function
|
|
1545
|
+
// decodes RLE rows of single 16bit or 32bit channel, each row is decoded into `row` buffer and then converted to values
|
|
1546
|
+
function decodeRLEChannelWide(
|
|
1473
1547
|
src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
|
|
1474
|
-
data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array
|
|
1548
|
+
data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array
|
|
1475
1549
|
) {
|
|
1476
1550
|
const srcEnd = src.length;
|
|
1477
1551
|
const rowSize = width * bpv;
|
|
@@ -1483,12 +1557,8 @@ function decodeRLEChannel(
|
|
|
1483
1557
|
|
|
1484
1558
|
if (rowStart >= rowEnd) continue;
|
|
1485
1559
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
} else {
|
|
1489
|
-
const decoded = decodeRLERow(src, rowStart, rowEnd, row!, 0, 1, rowSize);
|
|
1490
|
-
scatterValues(row!, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
|
|
1491
|
-
}
|
|
1560
|
+
const decoded = decodeRLERow(src, rowStart, rowEnd, row, rowSize);
|
|
1561
|
+
scatterValues(row, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
|
|
1492
1562
|
}
|
|
1493
1563
|
}
|
|
1494
1564
|
|
|
@@ -1496,15 +1566,19 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
|
|
|
1496
1566
|
const data = pixelData && pixelData.data;
|
|
1497
1567
|
const rows = offsets.length * height;
|
|
1498
1568
|
const lengthSize = large ? 4 : 2;
|
|
1569
|
+
const lengthsMemory = rows * lengthSize;
|
|
1570
|
+
const srcEnd = reader.view.byteLength;
|
|
1571
|
+
const bpv = bytesPerValue(bitDepth); // throws before consuming memory
|
|
1572
|
+
let lengths: Uint16Array | Uint32Array;
|
|
1499
1573
|
|
|
1500
|
-
consumeMemory(reader,
|
|
1574
|
+
consumeMemory(reader, lengthsMemory);
|
|
1501
1575
|
|
|
1502
1576
|
try {
|
|
1503
|
-
if ((reader.offset +
|
|
1577
|
+
if ((reader.offset + lengthsMemory) > srcEnd) {
|
|
1504
1578
|
throw new Error('RLE row lengths exceed buffer length');
|
|
1505
1579
|
}
|
|
1506
1580
|
|
|
1507
|
-
|
|
1581
|
+
lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
|
|
1508
1582
|
let total = 0;
|
|
1509
1583
|
|
|
1510
1584
|
if (large) {
|
|
@@ -1513,36 +1587,39 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
|
|
|
1513
1587
|
for (let i = 0; i < rows; i++) total += (lengths[i] = readUint16(reader));
|
|
1514
1588
|
}
|
|
1515
1589
|
|
|
1516
|
-
|
|
1517
|
-
const srcEnd = reader.view.byteLength;
|
|
1518
|
-
|
|
1519
|
-
if (total > (srcEnd - start)) {
|
|
1590
|
+
if (total > (srcEnd - reader.offset)) {
|
|
1520
1591
|
// broken or truncated file, decode only data that is available
|
|
1521
1592
|
warnOrThrow(reader, 'RLE data exceeds buffer length');
|
|
1522
1593
|
}
|
|
1523
1594
|
|
|
1524
|
-
const bpv = bytesPerValue(bitDepth);
|
|
1525
1595
|
if (data) verifyArrayType(data, bpv);
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
recoverMemory(reader, lengthsMemory);
|
|
1598
|
+
throw e;
|
|
1599
|
+
}
|
|
1526
1600
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1601
|
+
// decoding can't throw, it's kept outside of try block because V8 generates much slower code for loops inlined into try block
|
|
1602
|
+
const src = new Uint8Array(reader.view.buffer, reader.view.byteOffset, srcEnd);
|
|
1603
|
+
const row = (data && bpv > 1) ? new Uint8Array(width * bpv) : undefined;
|
|
1604
|
+
let pos = reader.offset;
|
|
1530
1605
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1606
|
+
for (let c = 0, li = 0; c < offsets.length; c++, li += height) {
|
|
1607
|
+
const offset = offsets[c] | 0;
|
|
1608
|
+
const channelStart = pos;
|
|
1534
1609
|
|
|
1535
|
-
|
|
1610
|
+
for (let y = 0; y < height; y++) pos += lengths[li + y];
|
|
1536
1611
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1612
|
+
if (data && offset >= 0 && offset < step) {
|
|
1613
|
+
if (bpv === 1) {
|
|
1614
|
+
decodeRLEChannel8(src, lengths, li, channelStart, width, height, data, offset, step);
|
|
1615
|
+
} else {
|
|
1616
|
+
decodeRLEChannelWide(src, lengths, li, channelStart, width, height, data, offset, step, bpv, row!);
|
|
1539
1617
|
}
|
|
1540
1618
|
}
|
|
1541
|
-
|
|
1542
|
-
reader.offset = pos;
|
|
1543
|
-
} finally {
|
|
1544
|
-
recoverMemory(reader, rows * lengthSize);
|
|
1545
1619
|
}
|
|
1620
|
+
|
|
1621
|
+
reader.offset = pos;
|
|
1622
|
+
recoverMemory(reader, lengthsMemory);
|
|
1546
1623
|
}
|
|
1547
1624
|
|
|
1548
1625
|
export function readSection<T>(
|
|
@@ -1690,21 +1767,15 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1690
1767
|
if (channelsCount > 64) throw new Error(`Invalid pattern channel count: ${channelsCount}`);
|
|
1691
1768
|
// channels cover whole pattern, each 2 bytes of RLE data can produce at most 128 bytes
|
|
1692
1769
|
if (width * height > (Math.min(end, reader.view.byteLength) - reader.offset) * 64) throw new Error('Pattern data out of bounds');
|
|
1693
|
-
const size = width * height * 4;
|
|
1694
|
-
consumeMemory(reader, size);
|
|
1695
|
-
const data = new Uint8Array(size);
|
|
1696
|
-
|
|
1697
|
-
for (let i = 3; i < data.byteLength; i += 4) {
|
|
1698
|
-
data[i] = 255;
|
|
1699
|
-
}
|
|
1700
1770
|
|
|
1701
1771
|
// first written arrays are color channels, next one is transparency (Photoshop stores it in the last, sheet mask, array)
|
|
1702
1772
|
const colorChannels = colorMode === ColorMode.RGB ? 3 : (colorMode === ColorMode.CMYK ? 4 : 1);
|
|
1703
|
-
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
|
|
1773
|
+
const isCmyk = colorMode === ColorMode.CMYK;
|
|
1774
|
+
const alphaTarget = isCmyk ? 4 : 3;
|
|
1775
|
+
const channels: { target: number; depth: number; compressionMode: number; cdata: Uint8Array; ctop: number; cleft: number; w: number; h: number; rowBytes: number; channelSize: number; }[] = [];
|
|
1776
|
+
let maxDecodeSize = 0;
|
|
1707
1777
|
|
|
1778
|
+
// validate all channels before allocating any image data
|
|
1708
1779
|
for (let i = 0, ch = 0; i < (channelsCount + 2) && reader.offset + 4 <= end; i++) {
|
|
1709
1780
|
const has = readUint32(reader);
|
|
1710
1781
|
if (!has) continue;
|
|
@@ -1738,72 +1809,119 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1738
1809
|
const h = cbottom - ctop;
|
|
1739
1810
|
const rowBytes = w * (depth / 8);
|
|
1740
1811
|
const channelSize = rowBytes * h;
|
|
1741
|
-
let bytes: Uint8Array;
|
|
1742
1812
|
|
|
1743
1813
|
if (compressionMode === 0) {
|
|
1744
1814
|
if (cdata.byteLength < channelSize) throw new Error('Pattern channel data out of bounds');
|
|
1745
|
-
bytes = cdata;
|
|
1746
1815
|
} else if (compressionMode === 1) {
|
|
1747
1816
|
// each 2 bytes of RLE data can produce at most 128 bytes
|
|
1748
1817
|
if (channelSize > cdata.byteLength * 64) throw new Error('Pattern channel data out of bounds');
|
|
1818
|
+
maxDecodeSize = Math.max(maxDecodeSize, channelSize);
|
|
1819
|
+
} else {
|
|
1820
|
+
throw new Error('Invalid pattern compression mode');
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
channels.push({ target, depth, compressionMode, cdata, ctop, cleft, w, h, rowBytes, channelSize });
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// check peak memory up front (image data, CMYK buffer and largest RLE decode buffer),
|
|
1827
|
+
// image data is consumed only after successful decoding so failed patterns don't reduce the limit
|
|
1828
|
+
const size = width * height * 4;
|
|
1829
|
+
const cmykSize = isCmyk ? width * height * 5 : 0;
|
|
1830
|
+
consumeMemory(reader, size + cmykSize + maxDecodeSize);
|
|
1831
|
+
recoverMemory(reader, size + cmykSize + maxDecodeSize);
|
|
1832
|
+
|
|
1833
|
+
const data = new Uint8Array(size);
|
|
1834
|
+
|
|
1835
|
+
for (let i = 3; i < data.byteLength; i += 4) {
|
|
1836
|
+
data[i] = 255;
|
|
1837
|
+
}
|
|
1749
1838
|
|
|
1750
|
-
|
|
1839
|
+
// CMYK channels are decoded into separate buffer (5 values per pixel, inverted, 0 = 100% ink) and converted to RGB at the end
|
|
1840
|
+
const cmyk = isCmyk ? new Uint8Array(cmykSize).fill(255) : undefined;
|
|
1841
|
+
|
|
1842
|
+
for (const { target, depth, compressionMode, cdata, ctop, cleft, w, h, rowBytes, channelSize } of channels) {
|
|
1843
|
+
let bytes = cdata;
|
|
1844
|
+
|
|
1845
|
+
if (compressionMode === 1) {
|
|
1751
1846
|
bytes = new Uint8Array(channelSize);
|
|
1752
1847
|
const cdataReader = createReader(cdata.buffer, cdata.byteOffset, cdata.byteLength);
|
|
1753
1848
|
readDataRLE(cdataReader, { data: bytes, width: rowBytes, height: h }, rowBytes, h, 8, 1, [0], false);
|
|
1754
|
-
} else {
|
|
1755
|
-
throw new Error('Invalid pattern compression mode');
|
|
1756
1849
|
}
|
|
1757
1850
|
|
|
1851
|
+
if (depth !== 8) bytes = patternChannelTo8bit(bytes, w * h, depth);
|
|
1852
|
+
|
|
1758
1853
|
const ox = cleft - left;
|
|
1759
1854
|
const oy = ctop - top;
|
|
1760
1855
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1856
|
+
if (cmyk) {
|
|
1857
|
+
copyPatternChannel(bytes, cmyk, w, h, (ox + oy * width) * 5 + target, width * 5, 5);
|
|
1858
|
+
} else if (target === 3 || colorMode === ColorMode.RGB) {
|
|
1859
|
+
copyPatternChannel(bytes, data, w, h, (ox + oy * width) * 4 + target, width * 4, 4);
|
|
1860
|
+
} else if (colorMode === ColorMode.Indexed) {
|
|
1861
|
+
const colors = new Uint8Array(256 * 3);
|
|
1862
|
+
|
|
1863
|
+
for (let i = 0; i < 256; i++) {
|
|
1864
|
+
colors[i * 3 + 0] = palette[i].r;
|
|
1865
|
+
colors[i * 3 + 1] = palette[i].g;
|
|
1866
|
+
colors[i * 3 + 2] = palette[i].b;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
for (let y = 0; y < h; y++) {
|
|
1870
|
+
for (let x = 0, src = y * w, dst = (ox + (y + oy) * width) * 4; x < w; x++, src++, dst += 4) {
|
|
1871
|
+
const index = bytes[src] * 3;
|
|
1872
|
+
data[dst + 0] = colors[index + 0];
|
|
1873
|
+
data[dst + 1] = colors[index + 1];
|
|
1874
|
+
data[dst + 2] = colors[index + 2];
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
} else { // grayscale
|
|
1878
|
+
for (let y = 0; y < h; y++) {
|
|
1879
|
+
for (let x = 0, src = y * w, dst = (ox + (y + oy) * width) * 4; x < w; x++, src++, dst += 4) {
|
|
1880
|
+
const value = bytes[src];
|
|
1777
1881
|
data[dst + 0] = value;
|
|
1778
1882
|
data[dst + 1] = value;
|
|
1779
1883
|
data[dst + 2] = value;
|
|
1780
|
-
} else {
|
|
1781
|
-
data[dst + target] = value;
|
|
1782
1884
|
}
|
|
1783
1885
|
}
|
|
1784
1886
|
}
|
|
1785
|
-
|
|
1786
|
-
if (compressionMode === 1) recoverMemory(reader, channelSize);
|
|
1787
1887
|
}
|
|
1788
1888
|
|
|
1789
1889
|
if (cmyk) {
|
|
1790
1890
|
cmykToRgb({ width, height, data: cmyk }, { width, height, data }, false);
|
|
1791
|
-
recoverMemory(reader, cmyk.byteLength);
|
|
1792
1891
|
}
|
|
1793
1892
|
|
|
1893
|
+
consumeMemory(reader, size);
|
|
1794
1894
|
reader.offset = end;
|
|
1795
1895
|
|
|
1796
1896
|
return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data };
|
|
1797
1897
|
}
|
|
1798
1898
|
|
|
1799
|
-
// converts big-endian 16bit or 32bit (float)
|
|
1800
|
-
function
|
|
1899
|
+
// converts big-endian 16bit or 32bit (float) values to 8bit values
|
|
1900
|
+
function patternChannelTo8bit(bytes: Uint8Array, count: number, depth: number) {
|
|
1901
|
+
const result = new Uint8Array(count);
|
|
1902
|
+
|
|
1801
1903
|
if (depth === 16) {
|
|
1802
|
-
|
|
1904
|
+
for (let i = 0; i < count; i++) {
|
|
1905
|
+
result[i] = Math.round(((bytes[i * 2] << 8) | bytes[i * 2 + 1]) / 257);
|
|
1906
|
+
}
|
|
1803
1907
|
} else {
|
|
1804
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset
|
|
1805
|
-
|
|
1806
|
-
|
|
1908
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1909
|
+
|
|
1910
|
+
for (let i = 0; i < count; i++) {
|
|
1911
|
+
const value = view.getFloat32(i * 4, false);
|
|
1912
|
+
result[i] = value > 0 ? (value < 1 ? Math.round(value * 255) : 255) : 0;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
return result;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// copies w x h channel values into interleaved data starting at `offset`
|
|
1920
|
+
function copyPatternChannel(bytes: Uint8Array, data: Uint8Array, w: number, h: number, offset: number, stride: number, step: number) {
|
|
1921
|
+
for (let y = 0, src = 0; y < h; y++) {
|
|
1922
|
+
for (let x = 0, dst = offset + y * stride; x < w; x++, src++, dst += step) {
|
|
1923
|
+
data[dst] = bytes[src];
|
|
1924
|
+
}
|
|
1807
1925
|
}
|
|
1808
1926
|
}
|
|
1809
1927
|
|