ag-psd 32.0.0 → 34.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.v8-cache/v24.7.0-x64-ef5a0af0/03e19709 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/05135617 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +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/cff9434a +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/e6745c2b +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/e6b99945 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/eeeebdfe +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
- package/CHANGELOG.md +100 -0
- package/LICENSE +5 -0
- package/README.md +4 -7
- package/README_PSD.md +261 -84
- package/TODO.md +13 -84
- package/dist/abr.d.ts +11 -4
- package/dist/abr.js +362 -230
- package/dist/abr.js.map +1 -1
- package/dist/additionalInfo.d.ts +56 -2
- package/dist/additionalInfo.js +828 -361
- package/dist/additionalInfo.js.map +1 -1
- package/dist/ase.d.ts +6 -1
- package/dist/ase.js +61 -61
- package/dist/ase.js.map +1 -1
- package/dist/bundle.js +3321 -1364
- package/dist/descriptor.d.ts +28 -13
- package/dist/descriptor.js +295 -241
- package/dist/descriptor.js.map +1 -1
- package/dist/helpers.d.ts +7 -1
- package/dist/helpers.js +246 -4
- package/dist/helpers.js.map +1 -1
- package/dist/imageResources.d.ts +15 -3
- package/dist/imageResources.js +586 -144
- 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/jpeg.js +8 -8
- package/dist/jpeg.js.map +1 -1
- package/dist/psd.d.ts +292 -19
- package/dist/psd.js.map +1 -1
- package/dist/psdReader.d.ts +10 -0
- package/dist/psdReader.js +424 -216
- package/dist/psdReader.js.map +1 -1
- package/dist/psdWriter.d.ts +2 -0
- package/dist/psdWriter.js +373 -113
- package/dist/psdWriter.js.map +1 -1
- package/dist/text.d.ts +32 -1
- package/dist/text.js +75 -6
- package/dist/text.js.map +1 -1
- package/dist/utf8.js +0 -3
- package/dist/utf8.js.map +1 -1
- package/dist-es/abr.d.ts +11 -4
- package/dist-es/abr.js +362 -230
- package/dist-es/abr.js.map +1 -1
- package/dist-es/additionalInfo.d.ts +56 -2
- package/dist-es/additionalInfo.js +828 -361
- package/dist-es/additionalInfo.js.map +1 -1
- package/dist-es/ase.d.ts +6 -1
- package/dist-es/ase.js +61 -61
- package/dist-es/ase.js.map +1 -1
- package/dist-es/descriptor.d.ts +28 -13
- package/dist-es/descriptor.js +295 -241
- package/dist-es/descriptor.js.map +1 -1
- package/dist-es/helpers.d.ts +7 -1
- package/dist-es/helpers.js +246 -4
- package/dist-es/helpers.js.map +1 -1
- package/dist-es/imageResources.d.ts +15 -3
- package/dist-es/imageResources.js +586 -144
- 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/jpeg.js +8 -8
- package/dist-es/jpeg.js.map +1 -1
- package/dist-es/psd.d.ts +292 -19
- package/dist-es/psd.js.map +1 -1
- package/dist-es/psdReader.d.ts +10 -0
- package/dist-es/psdReader.js +424 -216
- package/dist-es/psdReader.js.map +1 -1
- package/dist-es/psdWriter.d.ts +2 -0
- package/dist-es/psdWriter.js +373 -113
- package/dist-es/psdWriter.js.map +1 -1
- package/dist-es/text.d.ts +32 -1
- package/dist-es/text.js +75 -6
- package/dist-es/text.js.map +1 -1
- package/dist-es/utf8.js +0 -3
- package/dist-es/utf8.js.map +1 -1
- package/package.json +2 -2
- package/perf.js +312 -9
- package/src/abr.ts +382 -235
- package/src/additionalInfo.ts +1071 -482
- package/src/ase.ts +66 -61
- package/src/descriptor.ts +291 -242
- package/src/helpers.ts +266 -3
- package/src/imageResources.ts +736 -169
- package/src/index.ts +1 -0
- package/src/jpeg.ts +8 -8
- package/src/psd.ts +271 -32
- package/src/psdReader.ts +440 -221
- package/src/psdWriter.ts +402 -110
- package/src/text.ts +93 -7
- package/src/utf8.ts +0 -4
package/src/psdReader.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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,
|
|
3
|
+
import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, LayerMaskFlags, MaskParams, ColorSpace, largeAdditionalInfoKeys, largeAdditionalInfoKeysObserved, imageDataToCanvas, normalizeLabAB, cmykToRgbValues } from './helpers';
|
|
4
4
|
import { infoHandlersMap } from './additionalInfo';
|
|
5
|
+
import { decodeString } from './utf8';
|
|
5
6
|
import { InternalImageResources, resourceHandlersMap } from './imageResources';
|
|
6
7
|
|
|
7
8
|
interface ChannelInfo {
|
|
@@ -9,7 +10,7 @@ interface ChannelInfo {
|
|
|
9
10
|
length: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB, ColorMode.Indexed];
|
|
13
|
+
export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB, ColorMode.Indexed, ColorMode.CMYK];
|
|
13
14
|
const colorModes = ['bitmap', 'grayscale', 'indexed', 'RGB', 'CMYK', '', '', 'multichannel', 'duotone', 'lab'];
|
|
14
15
|
|
|
15
16
|
function setupGrayscale(data: PixelData) {
|
|
@@ -27,7 +28,44 @@ export interface PsdReader extends ReadOptions {
|
|
|
27
28
|
view: DataView;
|
|
28
29
|
large: boolean;
|
|
29
30
|
globalAlpha: boolean;
|
|
31
|
+
layersInGlobalInfo?: boolean; // layers were read from Lr16/Lr32/Layr global additional info
|
|
30
32
|
log(...args: any[]): void;
|
|
33
|
+
// text decoders, created lazily (null if not supported)
|
|
34
|
+
utf8Decoder?: TextDecoder | null;
|
|
35
|
+
utf8StrictDecoder?: TextDecoder | null;
|
|
36
|
+
gbkDecoder?: TextDecoder | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createTextDecoder(label: string, options?: TextDecoderOptions) {
|
|
40
|
+
try {
|
|
41
|
+
return typeof TextDecoder !== 'undefined' ? new TextDecoder(label, options) : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null; // encoding not supported
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// non-fatal UTF-8 decoder, malformed byte sequences are replaced with U+FFFD
|
|
48
|
+
export function getUtf8Decoder(reader: PsdReader) {
|
|
49
|
+
if (reader.utf8Decoder === undefined) reader.utf8Decoder = createTextDecoder('utf-8', { ignoreBOM: true });
|
|
50
|
+
return reader.utf8Decoder;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// UTF-8 decoder that throws on malformed byte sequences
|
|
54
|
+
export function getUtf8StrictDecoder(reader: PsdReader) {
|
|
55
|
+
if (reader.utf8StrictDecoder === undefined) reader.utf8StrictDecoder = createTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
56
|
+
return reader.utf8StrictDecoder;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// GBK decoder that throws on malformed byte sequences
|
|
60
|
+
export function getGbkStrictDecoder(reader: PsdReader) {
|
|
61
|
+
if (reader.gbkDecoder === undefined) reader.gbkDecoder = createTextDecoder('gbk', { fatal: true });
|
|
62
|
+
return reader.gbkDecoder;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decodeUtf8(reader: PsdReader, buffer: Uint8Array) {
|
|
66
|
+
// native decoder is faster only for longer strings
|
|
67
|
+
const decoder = buffer.byteLength > 1000 ? getUtf8Decoder(reader) : null;
|
|
68
|
+
return decoder ? decoder.decode(buffer) : decodeString(buffer);
|
|
31
69
|
}
|
|
32
70
|
|
|
33
71
|
export function createReader(buffer: ArrayBufferLike, offset?: number, length?: number): PsdReader {
|
|
@@ -147,6 +185,8 @@ export function readUnicodeString(reader: PsdReader) {
|
|
|
147
185
|
}
|
|
148
186
|
|
|
149
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;
|
|
150
190
|
|
|
151
191
|
function checkReadBounds(reader: PsdReader, size: number) {
|
|
152
192
|
if (reader.offset < 0 || (reader.offset + size) > reader.view.byteLength) {
|
|
@@ -160,10 +200,21 @@ function readUtf16String(reader: PsdReader, length: number, littleEndian: boolea
|
|
|
160
200
|
checkReadBounds(reader, length * 2);
|
|
161
201
|
|
|
162
202
|
const view = reader.view;
|
|
163
|
-
const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
|
|
164
203
|
let offset = reader.offset;
|
|
165
204
|
let text = '';
|
|
166
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
|
+
|
|
167
218
|
for (let i = 0; i < length; i += STRING_CHUNK_SIZE) {
|
|
168
219
|
const count = Math.min(STRING_CHUNK_SIZE, length - i);
|
|
169
220
|
|
|
@@ -203,6 +254,19 @@ export function readUnicodeStringWithLengthLE(reader: PsdReader, length: number)
|
|
|
203
254
|
export function readAsciiString(reader: PsdReader, length: number) {
|
|
204
255
|
if (!(length > 0)) return '';
|
|
205
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
|
+
|
|
206
270
|
const text = bytesToString(new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length));
|
|
207
271
|
reader.offset += length;
|
|
208
272
|
return text;
|
|
@@ -222,7 +286,16 @@ export function checkSignature(reader: PsdReader, a: string, b?: string) {
|
|
|
222
286
|
}
|
|
223
287
|
|
|
224
288
|
function readShortString(reader: PsdReader, length: number) {
|
|
225
|
-
|
|
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;
|
|
226
299
|
}
|
|
227
300
|
|
|
228
301
|
function isValidSignature(sig: string) {
|
|
@@ -244,7 +317,8 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
244
317
|
const maxSize = version === 1 ? 30000 : 300000;
|
|
245
318
|
|
|
246
319
|
if (width > maxSize || height > maxSize) throw new Error(`Invalid size: ${width}x${height}`);
|
|
247
|
-
|
|
320
|
+
// up to 56 channels plus transparency of composite image (Photoshop CS3 writes 57 channels for 53 alpha channels)
|
|
321
|
+
if (channels < 1 || channels > 57) throw new Error(`Invalid channel count: ${channels}`);
|
|
248
322
|
if (![1, 8, 16, 32].includes(bitsPerChannel)) throw new Error(`Invalid bitsPerChannel: ${bitsPerChannel}`);
|
|
249
323
|
if (supportedColorModes.indexOf(colorMode) === -1) throw new Error(`Color mode not supported: ${colorModes[colorMode] ?? colorMode}`);
|
|
250
324
|
|
|
@@ -253,6 +327,7 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
253
327
|
Object.assign(reader, readOptions);
|
|
254
328
|
reader.large = version === 2;
|
|
255
329
|
reader.globalAlpha = false;
|
|
330
|
+
reader.layersInGlobalInfo = false;
|
|
256
331
|
if (!('totalMemoryLimit' in reader)) { // setting totalMemoryLimit to undefined explicitly disables memory limit
|
|
257
332
|
reader.totalMemoryLimit = 2 * 1024 * 1024 * 1024; // default 2GB memory limit
|
|
258
333
|
}
|
|
@@ -280,12 +355,16 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
280
355
|
// image resources
|
|
281
356
|
|
|
282
357
|
const imageResources: InternalImageResources = {};
|
|
358
|
+
let unreadResourceBytes = 0;
|
|
283
359
|
|
|
284
360
|
readSection(reader, 1, left => {
|
|
285
|
-
|
|
286
|
-
|
|
361
|
+
// stop if there's not enough data left for resource header (some files have few trailing bytes in this section)
|
|
362
|
+
while (left() >= 12) {
|
|
363
|
+
const signature = realignWithSignature(reader, isValidSignature);
|
|
287
364
|
const id = readUint16(reader);
|
|
288
|
-
|
|
365
|
+
const nameLength = readUint8(reader);
|
|
366
|
+
const name = readBytes(reader, nameLength);
|
|
367
|
+
skipBytes(reader, (nameLength + 1) % 2); // pad to even size
|
|
289
368
|
|
|
290
369
|
readSection(reader, 2, left => {
|
|
291
370
|
const handler = resourceHandlersMap[id];
|
|
@@ -293,20 +372,29 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
293
372
|
|
|
294
373
|
if (handler && !skip) {
|
|
295
374
|
try {
|
|
296
|
-
handler.read(reader, imageResources, left);
|
|
375
|
+
handler.read(reader, imageResources, left, { signature, id, name, width, height });
|
|
297
376
|
} catch (e) {
|
|
298
377
|
if (reader.throwForMissingFeatures) throw e;
|
|
299
378
|
skipBytes(reader, left());
|
|
300
379
|
}
|
|
301
380
|
} else {
|
|
302
|
-
|
|
381
|
+
reader.logMissingFeatures && reader.log(`Unhandled image resource: ${id} (${left()})`);
|
|
303
382
|
skipBytes(reader, left());
|
|
304
383
|
}
|
|
305
384
|
});
|
|
306
385
|
}
|
|
386
|
+
|
|
387
|
+
unreadResourceBytes = left();
|
|
388
|
+
skipBytes(reader, left());
|
|
307
389
|
});
|
|
308
390
|
|
|
309
|
-
|
|
391
|
+
// some files have image resources section length few bytes larger than the data, layer and mask info section
|
|
392
|
+
// starts right after the last resource in that case
|
|
393
|
+
if (unreadResourceBytes && !sectionFits(reader, reader.offset) && sectionFits(reader, reader.offset - unreadResourceBytes)) {
|
|
394
|
+
reader.offset -= unreadResourceBytes;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const { layersGroup, layerGroupsEnabledId, savedPathsOrigination, ...rest } = imageResources;
|
|
310
398
|
|
|
311
399
|
if (Object.keys(rest).length) {
|
|
312
400
|
psd.imageResources = rest;
|
|
@@ -354,6 +442,7 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
354
442
|
const rawCompositeData = new Uint8Array(reader.view.buffer, reader.view.byteOffset + rawOffset, reader.view.byteLength - rawOffset);
|
|
355
443
|
psd.rawCompositeData = rawCompositeData;
|
|
356
444
|
psd.rawCompositeDataInfo = { large: reader.large, globalAlpha: reader.globalAlpha };
|
|
445
|
+
if (reader.layersInGlobalInfo) psd.rawCompositeDataInfo.layersInGlobalInfo = true;
|
|
357
446
|
} else {
|
|
358
447
|
const imageData = readImageData(reader, psd);
|
|
359
448
|
if (reader.useImageData) {
|
|
@@ -364,10 +453,6 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
364
453
|
}
|
|
365
454
|
}
|
|
366
455
|
|
|
367
|
-
// TODO: show converted color mode instead of original PSD file color mode
|
|
368
|
-
// but add option to preserve file color mode (need to return image data instead of canvas in that case)
|
|
369
|
-
// psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB
|
|
370
|
-
|
|
371
456
|
return psd;
|
|
372
457
|
}
|
|
373
458
|
|
|
@@ -408,6 +493,10 @@ export function readLayerInfo(reader: PsdReader, psd: Psd, imageResources: Inter
|
|
|
408
493
|
l.opened = type === SectionDividerType.OpenFolder;
|
|
409
494
|
l.children = [];
|
|
410
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
|
+
|
|
411
500
|
if (l.sectionDivider?.key) {
|
|
412
501
|
l.blendMode = toBlendMode[l.sectionDivider.key] ?? l.blendMode;
|
|
413
502
|
}
|
|
@@ -452,8 +541,11 @@ function readLayerRecord(reader: PsdReader, psd: Psd, imageResources: InternalIm
|
|
|
452
541
|
|
|
453
542
|
checkSignature(reader, '8BIM');
|
|
454
543
|
const blendMode = readSignature(reader);
|
|
455
|
-
if (!toBlendMode[blendMode])
|
|
456
|
-
|
|
544
|
+
if (!toBlendMode[blendMode]) {
|
|
545
|
+
if (reader.throwForMissingFeatures) throw new Error(`Invalid blend mode: '${blendMode}'`);
|
|
546
|
+
reader.logMissingFeatures && reader.log(`Unknown blend mode: '${blendMode}', using normal blend mode`);
|
|
547
|
+
}
|
|
548
|
+
layer.blendMode = toBlendMode[blendMode] || 'normal';
|
|
457
549
|
|
|
458
550
|
layer.opacity = readUint8(reader) / 0xff;
|
|
459
551
|
layer.clipping = readUint8(reader) === 1;
|
|
@@ -487,11 +579,20 @@ function readLayerRecord(reader: PsdReader, psd: Psd, imageResources: InternalIm
|
|
|
487
579
|
return { layer, channels };
|
|
488
580
|
}
|
|
489
581
|
|
|
582
|
+
// negative sizes are treated as empty (Photoshop writes them for some empty layers, like 0,0,-1,0 bounds of fill layer)
|
|
583
|
+
// checks if length of section (layer and mask info) at given offset fits in the file
|
|
584
|
+
function sectionFits(reader: PsdReader, offset: number) {
|
|
585
|
+
const lengthSize = reader.large ? 8 : 4;
|
|
586
|
+
if (offset < 0 || offset + lengthSize > reader.view.byteLength) return false;
|
|
587
|
+
const length = reader.large ? reader.view.getUint32(offset) * 0x100000000 + reader.view.getUint32(offset + 4) : reader.view.getUint32(offset);
|
|
588
|
+
return offset + lengthSize + length <= reader.view.byteLength;
|
|
589
|
+
}
|
|
590
|
+
|
|
490
591
|
function isValidBoxSize(box: { top?: number; left?: number; bottom?: number; right?: number; }, reader: PsdReader) {
|
|
491
592
|
const width = (box.right || 0) - (box.left || 0);
|
|
492
593
|
const height = (box.bottom || 0) - (box.top || 0);
|
|
493
594
|
const maxSize = reader.large ? 300000 : 30000;
|
|
494
|
-
return width
|
|
595
|
+
return width <= maxSize && height <= maxSize;
|
|
495
596
|
}
|
|
496
597
|
|
|
497
598
|
function readLayerMaskData(reader: PsdReader, layer: Layer) {
|
|
@@ -613,8 +714,8 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
|
|
|
613
714
|
let compression = Compression.RawData;
|
|
614
715
|
let data: Uint8Array | undefined = undefined;
|
|
615
716
|
|
|
616
|
-
|
|
617
|
-
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) {
|
|
618
719
|
compression = readUint16(reader) as Compression;
|
|
619
720
|
// try to fix broken files where there's 1 byte shift of channel
|
|
620
721
|
if (compression > 3) {
|
|
@@ -622,9 +723,9 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
|
|
|
622
723
|
compression = readUint16(reader) as Compression;
|
|
623
724
|
}
|
|
624
725
|
|
|
625
|
-
// try to fix broken files where there's 1 byte shift of channel
|
|
726
|
+
// try to fix broken files where there's 1 byte shift of channel (in the other direction)
|
|
626
727
|
if (compression > 3) {
|
|
627
|
-
reader.offset -=
|
|
728
|
+
reader.offset -= 4;
|
|
628
729
|
compression = readUint16(reader) as Compression;
|
|
629
730
|
}
|
|
630
731
|
|
|
@@ -640,7 +741,14 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
|
|
|
640
741
|
}
|
|
641
742
|
|
|
642
743
|
if (!reader.useRawData) {
|
|
643
|
-
|
|
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
|
+
}
|
|
644
752
|
}
|
|
645
753
|
}
|
|
646
754
|
|
|
@@ -665,6 +773,7 @@ export function getCompositeImageData(psd: Psd) {
|
|
|
665
773
|
if (info) {
|
|
666
774
|
reader.large = info.large;
|
|
667
775
|
reader.globalAlpha = info.globalAlpha;
|
|
776
|
+
reader.layersInGlobalInfo = !!info.layersInGlobalInfo;
|
|
668
777
|
}
|
|
669
778
|
|
|
670
779
|
return readImageData(reader, psd);
|
|
@@ -760,18 +869,13 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
760
869
|
|
|
761
870
|
if (layerWidth && layerHeight && read === LayerDataType.Layer) {
|
|
762
871
|
if (cmyk) {
|
|
763
|
-
if (bitsPerChannel !== 8) throw new Error('bitsPerChannel not supported');
|
|
764
|
-
imageData =
|
|
872
|
+
if (bitsPerChannel !== 8 && bitsPerChannel !== 16) throw new Error('bitsPerChannel not supported');
|
|
873
|
+
imageData = createImageDataBitDepth(layerWidth, layerHeight, bitsPerChannel, 5, memoryLimit);
|
|
765
874
|
} else {
|
|
766
875
|
imageData = createImageDataBitDepth(layerWidth, layerHeight, bitsPerChannel, 4, memoryLimit);
|
|
767
876
|
}
|
|
768
877
|
}
|
|
769
878
|
|
|
770
|
-
if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
|
|
771
|
-
(layer as any).imageDataRaw = [];
|
|
772
|
-
(layer as any).imageDataRawCompression = [];
|
|
773
|
-
}
|
|
774
|
-
|
|
775
879
|
for (const { id, compression, data } of channels) {
|
|
776
880
|
if (!data) continue;
|
|
777
881
|
|
|
@@ -794,15 +898,6 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
794
898
|
maskData = createImageDataBitDepth(maskWidth, maskHeight, bitsPerChannel, 4, memoryLimit);
|
|
795
899
|
if (memoryLimit !== undefined) dataReader.totalMemoryLimit = Math.max(0, dataReader.totalMemoryLimit! - maskData.data.byteLength);
|
|
796
900
|
readData(dataReader, data.byteLength, maskData, compression, maskWidth, maskHeight, bitsPerChannel, 0, large, 4);
|
|
797
|
-
if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
|
|
798
|
-
if (id === ChannelID.UserMask) {
|
|
799
|
-
(layer as any).maskDataRawCompression = compression;
|
|
800
|
-
(layer as any).maskDataRaw = data;
|
|
801
|
-
} else {
|
|
802
|
-
(layer as any).realMaskDataRawCompression = compression;
|
|
803
|
-
(layer as any).realMaskDataRaw = data;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
901
|
setupGrayscale(maskData);
|
|
807
902
|
resetAlpha(maskData, false);
|
|
808
903
|
}
|
|
@@ -821,12 +916,6 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
821
916
|
}
|
|
822
917
|
|
|
823
918
|
readData(dataReader, data.byteLength, targetData, compression, layerWidth, layerHeight, bitsPerChannel, offset, large, cmyk ? 5 : 4);
|
|
824
|
-
|
|
825
|
-
if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
|
|
826
|
-
(layer as any).imageDataRawCompression[id] = compression;
|
|
827
|
-
(layer as any).imageDataRaw[id] = data;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
919
|
}
|
|
831
920
|
|
|
832
921
|
if (id === ChannelID.Transparency) {
|
|
@@ -840,7 +929,8 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
840
929
|
|
|
841
930
|
if (cmyk) {
|
|
842
931
|
const cmykData = imageData;
|
|
843
|
-
|
|
932
|
+
const rgbMemoryLimit = memoryLimit === undefined ? undefined : Math.max(0, memoryLimit - cmykData.data.byteLength);
|
|
933
|
+
imageData = createImageDataBitDepth(cmykData.width, cmykData.height, bitsPerChannel, 4, rgbMemoryLimit);
|
|
844
934
|
cmykToRgb(cmykData, imageData, false);
|
|
845
935
|
}
|
|
846
936
|
}
|
|
@@ -880,7 +970,7 @@ export function readGlobalLayerMaskInfo(reader: PsdReader) {
|
|
|
880
970
|
const colorSpace2 = readUint16(reader);
|
|
881
971
|
const colorSpace3 = readUint16(reader);
|
|
882
972
|
const colorSpace4 = readUint16(reader);
|
|
883
|
-
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)
|
|
884
974
|
const kind = readUint8(reader);
|
|
885
975
|
skipBytes(reader, left()); // 3 bytes of padding ?
|
|
886
976
|
return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind };
|
|
@@ -918,7 +1008,16 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition
|
|
|
918
1008
|
const key = readSignature(reader);
|
|
919
1009
|
|
|
920
1010
|
// `largeAdditionalInfoKeys` fallback, because some keys don't have 8B64 signature even when they are 64bit
|
|
921
|
-
|
|
1011
|
+
let u64 = sig === '8B64' || (reader.large && largeAdditionalInfoKeys.indexOf(key) !== -1);
|
|
1012
|
+
|
|
1013
|
+
// these keys were written by older versions of this library with 32bit length and 8BIM signature,
|
|
1014
|
+
// 64bit length always has high 32bits set to 0 (lengths over 4GB are not supported)
|
|
1015
|
+
if (reader.large && sig === '8BIM' && largeAdditionalInfoKeysObserved.indexOf(key) !== -1) {
|
|
1016
|
+
// 32bit length of 0 is followed by signature of the next block, 64bit length would continue with its low 32bits
|
|
1017
|
+
const next = reader.offset + 8 <= reader.view.byteLength ? reader.view.getUint32(reader.offset + 4) : 0;
|
|
1018
|
+
const nextIsSignature = next === 0x3842494d /* 8BIM */ || next === 0x38423634 /* 8B64 */;
|
|
1019
|
+
u64 = reader.view.getUint32(reader.offset) === 0 && !nextIsSignature;
|
|
1020
|
+
}
|
|
922
1021
|
|
|
923
1022
|
readSection(reader, 2, left => {
|
|
924
1023
|
const handler = infoHandlersMap[key];
|
|
@@ -930,7 +1029,7 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition
|
|
|
930
1029
|
if (reader.throwForMissingFeatures) throw e;
|
|
931
1030
|
}
|
|
932
1031
|
} else {
|
|
933
|
-
reader.logMissingFeatures && reader.log(`Unhandled additional info: ${key}`);
|
|
1032
|
+
reader.logMissingFeatures && key !== 'GenI' && key !== 'OCIO' && key !== 'CAI ' && reader.log(`Unhandled additional info: ${key}`);
|
|
934
1033
|
skipBytes(reader, left());
|
|
935
1034
|
}
|
|
936
1035
|
|
|
@@ -996,14 +1095,8 @@ function readImageData(reader: PsdReader, psd: Psd) {
|
|
|
996
1095
|
case ColorMode.Grayscale: {
|
|
997
1096
|
const colorChannels = psd.colorMode === ColorMode.Grayscale ? 1 : 3;
|
|
998
1097
|
const offsets = compositeChannelOffsets(reader, psd, colorChannels, 3);
|
|
999
|
-
const start = reader.offset;
|
|
1000
|
-
|
|
1001
1098
|
readCompositeChannels(reader, compression, imageData, bitsPerChannel, 4, offsets);
|
|
1002
1099
|
|
|
1003
|
-
if (RAW_IMAGE_DATA && compression === Compression.RleCompressed) {
|
|
1004
|
-
(psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
1100
|
if (psd.colorMode === ColorMode.Grayscale) {
|
|
1008
1101
|
setupGrayscale(imageData);
|
|
1009
1102
|
}
|
|
@@ -1016,29 +1109,25 @@ function readImageData(reader: PsdReader, psd: Psd) {
|
|
|
1016
1109
|
const offsets = compositeChannelOffsets(reader, psd, 1, -1);
|
|
1017
1110
|
const indexedImageData: PixelData = { width, height, data: new Uint8Array(width * height) };
|
|
1018
1111
|
readCompositeChannels(reader, compression, indexedImageData, 8, 1, offsets);
|
|
1019
|
-
indexedToRgb(indexedImageData, imageData, psd.palette);
|
|
1112
|
+
indexedToRgb(indexedImageData, imageData, psd.palette, psd.imageResources?.transparencyIndex ?? -1);
|
|
1020
1113
|
break;
|
|
1021
1114
|
}
|
|
1022
1115
|
case ColorMode.CMYK: {
|
|
1023
|
-
if (bitsPerChannel !== 8) throw new Error('bitsPerChannel not supported');
|
|
1116
|
+
if (bitsPerChannel !== 8 && bitsPerChannel !== 16) throw new Error('bitsPerChannel not supported');
|
|
1024
1117
|
|
|
1025
1118
|
const offsets = compositeChannelOffsets(reader, psd, 4, 4);
|
|
1026
|
-
const cmykImageData
|
|
1027
|
-
const start = reader.offset;
|
|
1119
|
+
const cmykImageData = createImageDataBitDepth(width, height, bitsPerChannel, 5, reader.totalMemoryLimit);
|
|
1028
1120
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (RAW_IMAGE_DATA && compression === Compression.RleCompressed) {
|
|
1033
|
-
(psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
1034
|
-
}
|
|
1121
|
+
resetAlpha(cmykImageData, true);
|
|
1122
|
+
readCompositeChannels(reader, compression, cmykImageData, bitsPerChannel, 5, offsets);
|
|
1123
|
+
cmykToRgb(cmykImageData, imageData, !!reader.globalAlpha);
|
|
1035
1124
|
break;
|
|
1036
1125
|
}
|
|
1037
1126
|
default: throw new Error(`Color mode not supported: ${psd.colorMode}`);
|
|
1038
1127
|
}
|
|
1039
1128
|
|
|
1040
|
-
// remove weird white matte
|
|
1041
|
-
if (reader.globalAlpha) {
|
|
1129
|
+
// remove weird white matte (already removed when converting from CMYK)
|
|
1130
|
+
if (reader.globalAlpha && psd.colorMode !== ColorMode.CMYK) {
|
|
1042
1131
|
removeWhiteMatte(imageData);
|
|
1043
1132
|
}
|
|
1044
1133
|
|
|
@@ -1053,8 +1142,12 @@ function compositeChannelOffsets(reader: PsdReader, psd: Psd, colorChannels: num
|
|
|
1053
1142
|
// First extra channel is transparency if document has global alpha (negative layer count). Some non-Photoshop
|
|
1054
1143
|
// applications write transparency channel without setting global alpha, in that case extra channel is not
|
|
1055
1144
|
// listed in alpha channel names (Photoshop names all user alpha channels / saved selections).
|
|
1056
|
-
|
|
1057
|
-
|
|
1145
|
+
// Photoshop also lists merged transparency as first alpha channel named "Transparency" (in documents marked with
|
|
1146
|
+
// Mtrn/Mt16/Mt32 additional info, or in some 16bit documents with layers stored in Lr16 without any other mark)
|
|
1147
|
+
const alphaNames = psd.imageResources?.alphaChannelNames ?? [];
|
|
1148
|
+
const namedChannels = alphaNames.length;
|
|
1149
|
+
const namedTransparency = !!reader.layersInGlobalInfo && alphaNames[0] === 'Transparency' && namedChannels === channelCount - colorChannels;
|
|
1150
|
+
const hasTransparency = reader.globalAlpha || namedTransparency || (channelCount - colorChannels) > namedChannels;
|
|
1058
1151
|
const offsets: number[] = [];
|
|
1059
1152
|
|
|
1060
1153
|
for (let i = 0; i < channelCount; i++) {
|
|
@@ -1138,45 +1231,61 @@ function removeWhiteMatte({ data, width, height }: PixelData) {
|
|
|
1138
1231
|
}
|
|
1139
1232
|
}
|
|
1140
1233
|
|
|
1141
|
-
|
|
1234
|
+
// converts value in 0-255 range to target range, rounding and clamping it
|
|
1235
|
+
function toRange(value: number, max: number) {
|
|
1236
|
+
value = Math.round(value * (max / 255));
|
|
1237
|
+
return value <= 0 ? 0 : (value >= max ? max : value);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// converts CMYK data to RGB using approximation of U.S. Web Coated (SWOP) v2 profile (see cmykToRgbValues)
|
|
1241
|
+
// cmyk - 8 or 16 bit data with 5 channels (cmyka), rgb - 8 or 16 bit data with 4 channels (rgba)
|
|
1242
|
+
// removeMatte - colors are blended with white (no ink), used for composite image of documents with global alpha
|
|
1243
|
+
function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
|
|
1142
1244
|
const size = rgb.width * rgb.height * 4;
|
|
1143
1245
|
const srcData = cmyk.data;
|
|
1144
1246
|
const dstData = rgb.data;
|
|
1247
|
+
const srcMax = srcData instanceof Uint16Array ? 0xffff : 0xff;
|
|
1248
|
+
const dstMax = dstData instanceof Uint16Array ? 0xffff : 0xff;
|
|
1249
|
+
const out = [0, 0, 0];
|
|
1145
1250
|
|
|
1146
1251
|
for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1252
|
+
const a = srcData[src + 4];
|
|
1253
|
+
|
|
1254
|
+
// image data stores inverted values (0 = 100% ink)
|
|
1255
|
+
let c = 1 - srcData[src] / srcMax;
|
|
1256
|
+
let m = 1 - srcData[src + 1] / srcMax;
|
|
1257
|
+
let y = 1 - srcData[src + 2] / srcMax;
|
|
1258
|
+
let k = 1 - srcData[src + 3] / srcMax;
|
|
1259
|
+
|
|
1260
|
+
if (removeMatte && a > 0 && a < srcMax) {
|
|
1261
|
+
const ra = srcMax / a;
|
|
1262
|
+
c = Math.min(1, c * ra);
|
|
1263
|
+
m = Math.min(1, m * ra);
|
|
1264
|
+
y = Math.min(1, y * ra);
|
|
1265
|
+
k = Math.min(1, k * ra);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
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);
|
|
1272
|
+
dstData[dst + 3] = srcMax === dstMax ? a : Math.round(a * dstMax / srcMax);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// transparencyIndex - palette index of transparent color (-1 if there's none)
|
|
1277
|
+
function indexedToRgb(indexed: PixelData, rgb: PixelData, palette: RGB[], transparencyIndex: number) {
|
|
1170
1278
|
const size = indexed.width * indexed.height;
|
|
1171
1279
|
const srcData = indexed.data;
|
|
1172
1280
|
const dstData = rgb.data;
|
|
1173
1281
|
|
|
1174
1282
|
for (let src = 0, dst = 0; src < size; src++, dst += 4) {
|
|
1175
|
-
const
|
|
1283
|
+
const index = srcData[src];
|
|
1284
|
+
const c = palette[index];
|
|
1176
1285
|
dstData[dst + 0] = c.r;
|
|
1177
1286
|
dstData[dst + 1] = c.g;
|
|
1178
1287
|
dstData[dst + 2] = c.b;
|
|
1179
|
-
dstData[dst + 3] = 255;
|
|
1288
|
+
dstData[dst + 3] = index === transparencyIndex ? 0 : 255;
|
|
1180
1289
|
}
|
|
1181
1290
|
}
|
|
1182
1291
|
|
|
@@ -1342,8 +1451,8 @@ export function readDataZip(
|
|
|
1342
1451
|
}
|
|
1343
1452
|
}
|
|
1344
1453
|
|
|
1345
|
-
// decodes single PackBits row, returns number of decoded
|
|
1346
|
-
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) {
|
|
1347
1456
|
let x = 0;
|
|
1348
1457
|
|
|
1349
1458
|
while (i < end && x < count) {
|
|
@@ -1355,19 +1464,15 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
|
|
|
1355
1464
|
const value = src[i++];
|
|
1356
1465
|
let n = 257 - header;
|
|
1357
1466
|
if (n > count - x) n = count - x;
|
|
1467
|
+
dst.fill(value, x, x + n);
|
|
1358
1468
|
x += n;
|
|
1359
|
-
|
|
1360
|
-
for (; n > 0; n--, p += step) {
|
|
1361
|
-
dst[p] = value;
|
|
1362
|
-
}
|
|
1363
1469
|
} else if (header < 128) {
|
|
1364
1470
|
let n = header + 1;
|
|
1365
1471
|
if (n > count - x) n = count - x;
|
|
1366
1472
|
if (n > end - i) n = end - i;
|
|
1367
|
-
x += n;
|
|
1368
1473
|
|
|
1369
|
-
for (; n > 0; n
|
|
1370
|
-
dst[
|
|
1474
|
+
for (; n > 0; n--) {
|
|
1475
|
+
dst[x++] = src[i++];
|
|
1371
1476
|
}
|
|
1372
1477
|
} else {
|
|
1373
1478
|
// ignore 128
|
|
@@ -1377,6 +1482,51 @@ function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray,
|
|
|
1377
1482
|
return x;
|
|
1378
1483
|
}
|
|
1379
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
|
+
|
|
1380
1530
|
// converts big-endian 16/32-bit values from bytes and writes them to `data` starting at `p` with `step`
|
|
1381
1531
|
function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: PixelArray, p: number, step: number) {
|
|
1382
1532
|
if (bpv === 2) {
|
|
@@ -1392,10 +1542,10 @@ function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: Pixe
|
|
|
1392
1542
|
}
|
|
1393
1543
|
}
|
|
1394
1544
|
|
|
1395
|
-
// decodes RLE rows of single channel, each row
|
|
1396
|
-
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(
|
|
1397
1547
|
src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
|
|
1398
|
-
data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array
|
|
1548
|
+
data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array
|
|
1399
1549
|
) {
|
|
1400
1550
|
const srcEnd = src.length;
|
|
1401
1551
|
const rowSize = width * bpv;
|
|
@@ -1407,12 +1557,8 @@ function decodeRLEChannel(
|
|
|
1407
1557
|
|
|
1408
1558
|
if (rowStart >= rowEnd) continue;
|
|
1409
1559
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
} else {
|
|
1413
|
-
const decoded = decodeRLERow(src, rowStart, rowEnd, row!, 0, 1, rowSize);
|
|
1414
|
-
scatterValues(row!, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
|
|
1415
|
-
}
|
|
1560
|
+
const decoded = decodeRLERow(src, rowStart, rowEnd, row, rowSize);
|
|
1561
|
+
scatterValues(row, Math.floor(decoded / bpv), bpv, data, offset + y * width * step, step);
|
|
1416
1562
|
}
|
|
1417
1563
|
}
|
|
1418
1564
|
|
|
@@ -1420,15 +1566,19 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
|
|
|
1420
1566
|
const data = pixelData && pixelData.data;
|
|
1421
1567
|
const rows = offsets.length * height;
|
|
1422
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;
|
|
1423
1573
|
|
|
1424
|
-
consumeMemory(reader,
|
|
1574
|
+
consumeMemory(reader, lengthsMemory);
|
|
1425
1575
|
|
|
1426
1576
|
try {
|
|
1427
|
-
if ((reader.offset +
|
|
1577
|
+
if ((reader.offset + lengthsMemory) > srcEnd) {
|
|
1428
1578
|
throw new Error('RLE row lengths exceed buffer length');
|
|
1429
1579
|
}
|
|
1430
1580
|
|
|
1431
|
-
|
|
1581
|
+
lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
|
|
1432
1582
|
let total = 0;
|
|
1433
1583
|
|
|
1434
1584
|
if (large) {
|
|
@@ -1437,36 +1587,39 @@ export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined,
|
|
|
1437
1587
|
for (let i = 0; i < rows; i++) total += (lengths[i] = readUint16(reader));
|
|
1438
1588
|
}
|
|
1439
1589
|
|
|
1440
|
-
|
|
1441
|
-
const srcEnd = reader.view.byteLength;
|
|
1442
|
-
|
|
1443
|
-
if (total > (srcEnd - start)) {
|
|
1590
|
+
if (total > (srcEnd - reader.offset)) {
|
|
1444
1591
|
// broken or truncated file, decode only data that is available
|
|
1445
1592
|
warnOrThrow(reader, 'RLE data exceeds buffer length');
|
|
1446
1593
|
}
|
|
1447
1594
|
|
|
1448
|
-
const bpv = bytesPerValue(bitDepth);
|
|
1449
1595
|
if (data) verifyArrayType(data, bpv);
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
recoverMemory(reader, lengthsMemory);
|
|
1598
|
+
throw e;
|
|
1599
|
+
}
|
|
1450
1600
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
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;
|
|
1454
1605
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1606
|
+
for (let c = 0, li = 0; c < offsets.length; c++, li += height) {
|
|
1607
|
+
const offset = offsets[c] | 0;
|
|
1608
|
+
const channelStart = pos;
|
|
1458
1609
|
|
|
1459
|
-
|
|
1610
|
+
for (let y = 0; y < height; y++) pos += lengths[li + y];
|
|
1460
1611
|
|
|
1461
|
-
|
|
1462
|
-
|
|
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!);
|
|
1463
1617
|
}
|
|
1464
1618
|
}
|
|
1465
|
-
|
|
1466
|
-
reader.offset = pos;
|
|
1467
|
-
} finally {
|
|
1468
|
-
recoverMemory(reader, rows * lengthSize);
|
|
1469
1619
|
}
|
|
1620
|
+
|
|
1621
|
+
reader.offset = pos;
|
|
1622
|
+
recoverMemory(reader, lengthsMemory);
|
|
1470
1623
|
}
|
|
1471
1624
|
|
|
1472
1625
|
export function readSection<T>(
|
|
@@ -1520,11 +1673,11 @@ export function readColor(reader: PsdReader): Color {
|
|
|
1520
1673
|
skipBytes(reader, 2);
|
|
1521
1674
|
return { h, s, b };
|
|
1522
1675
|
}
|
|
1523
|
-
case ColorSpace.CMYK: {
|
|
1524
|
-
const c = readUint16(reader) / 257;
|
|
1525
|
-
const m = readUint16(reader) / 257;
|
|
1526
|
-
const y = readUint16(reader) / 257;
|
|
1527
|
-
const k = readUint16(reader) / 257;
|
|
1676
|
+
case ColorSpace.CMYK: { // 0 = 100% ink
|
|
1677
|
+
const c = (0xffff - readUint16(reader)) / 257;
|
|
1678
|
+
const m = (0xffff - readUint16(reader)) / 257;
|
|
1679
|
+
const y = (0xffff - readUint16(reader)) / 257;
|
|
1680
|
+
const k = (0xffff - readUint16(reader)) / 257;
|
|
1528
1681
|
return { c, m, y, k };
|
|
1529
1682
|
}
|
|
1530
1683
|
case ColorSpace.Lab: {
|
|
@@ -1546,6 +1699,24 @@ export function readColor(reader: PsdReader): Color {
|
|
|
1546
1699
|
}
|
|
1547
1700
|
}
|
|
1548
1701
|
|
|
1702
|
+
const MAX_PATTERN_SIZE = 30000;
|
|
1703
|
+
|
|
1704
|
+
// reads pattern, unsupported or invalid patterns are skipped (returns undefined) unless throwForMissingFeatures is set
|
|
1705
|
+
export function readPatternSafe(reader: PsdReader): PatternInfo | undefined {
|
|
1706
|
+
let length = reader.view.getUint32(reader.offset);
|
|
1707
|
+
while (length % 4) length++;
|
|
1708
|
+
const end = reader.offset + 4 + length;
|
|
1709
|
+
|
|
1710
|
+
try {
|
|
1711
|
+
return readPattern(reader);
|
|
1712
|
+
} catch (e) {
|
|
1713
|
+
if (reader.throwForMissingFeatures) throw e;
|
|
1714
|
+
reader.logMissingFeatures && reader.log(`Skipped pattern: ${(e as Error).message}`);
|
|
1715
|
+
reader.offset = end;
|
|
1716
|
+
return undefined;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1549
1720
|
export function readPattern(reader: PsdReader): PatternInfo {
|
|
1550
1721
|
let length = readUint32(reader);
|
|
1551
1722
|
while (length % 4) length++;
|
|
@@ -1557,8 +1728,8 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1557
1728
|
const x = readInt16(reader);
|
|
1558
1729
|
const y = readInt16(reader);
|
|
1559
1730
|
|
|
1560
|
-
// we only support RGB and
|
|
1561
|
-
if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed) {
|
|
1731
|
+
// we only support RGB, grayscale, indexed and CMYK for now
|
|
1732
|
+
if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed && colorMode !== ColorMode.CMYK) {
|
|
1562
1733
|
throw new Error(`Unsupported pattern color mode: ${colorMode}`);
|
|
1563
1734
|
}
|
|
1564
1735
|
|
|
@@ -1575,7 +1746,8 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1575
1746
|
})
|
|
1576
1747
|
}
|
|
1577
1748
|
|
|
1578
|
-
|
|
1749
|
+
// some writers follow the palette with 4 extra bytes (unknown)
|
|
1750
|
+
if (reader.view.getUint32(reader.offset) !== 3) skipBytes(reader, 4);
|
|
1579
1751
|
}
|
|
1580
1752
|
|
|
1581
1753
|
// virtual memory array list
|
|
@@ -1590,19 +1762,28 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1590
1762
|
const channelsCount = readUint32(reader);
|
|
1591
1763
|
const width = right - left;
|
|
1592
1764
|
const height = bottom - top;
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1765
|
+
if (width < 0 || height < 0) throw new Error('Invalid pattern bounds');
|
|
1766
|
+
if (width > MAX_PATTERN_SIZE || height > MAX_PATTERN_SIZE) throw new Error(`Pattern too large (${width}x${height})`);
|
|
1767
|
+
if (channelsCount > 64) throw new Error(`Invalid pattern channel count: ${channelsCount}`);
|
|
1768
|
+
// channels cover whole pattern, each 2 bytes of RLE data can produce at most 128 bytes
|
|
1769
|
+
if (width * height > (Math.min(end, reader.view.byteLength) - reader.offset) * 64) throw new Error('Pattern data out of bounds');
|
|
1770
|
+
|
|
1771
|
+
// first written arrays are color channels, next one is transparency (Photoshop stores it in the last, sheet mask, array)
|
|
1772
|
+
const colorChannels = colorMode === ColorMode.RGB ? 3 : (colorMode === ColorMode.CMYK ? 4 : 1);
|
|
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;
|
|
1777
|
+
|
|
1778
|
+
// validate all channels before allocating any image data
|
|
1779
|
+
for (let i = 0, ch = 0; i < (channelsCount + 2) && reader.offset + 4 <= end; i++) {
|
|
1602
1780
|
const has = readUint32(reader);
|
|
1603
1781
|
if (!has) continue;
|
|
1604
1782
|
|
|
1605
1783
|
const length = readUint32(reader);
|
|
1784
|
+
if (!length) continue;
|
|
1785
|
+
if (length < 23) throw new Error('Invalid pattern channel length');
|
|
1786
|
+
|
|
1606
1787
|
const pixelDepth = readUint32(reader);
|
|
1607
1788
|
const ctop = readUint32(reader);
|
|
1608
1789
|
const cleft = readUint32(reader);
|
|
@@ -1610,105 +1791,143 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1610
1791
|
const cright = readUint32(reader);
|
|
1611
1792
|
const pixelDepth2 = readUint16(reader);
|
|
1612
1793
|
const compressionMode = readUint8(reader); // 0 - raw, 1 - rle
|
|
1613
|
-
const
|
|
1614
|
-
const
|
|
1794
|
+
const cdata = readBytes(reader, length - 23);
|
|
1795
|
+
const depth = (pixelDepth2 === 8 || pixelDepth2 === 16 || pixelDepth2 === 32) ? pixelDepth2 : pixelDepth;
|
|
1796
|
+
|
|
1797
|
+
if (depth !== 8 && depth !== 16 && depth !== 32) throw new Error(`Unsupported pattern pixel depth: ${depth}`);
|
|
1615
1798
|
|
|
1616
|
-
if (
|
|
1617
|
-
throw new Error('
|
|
1799
|
+
if (ctop < top || cleft < left || cbottom > bottom || cright > right || cbottom < ctop || cright < cleft) {
|
|
1800
|
+
throw new Error('Pattern channel bounds outside of pattern bounds');
|
|
1618
1801
|
}
|
|
1619
1802
|
|
|
1803
|
+
const target = ch < colorChannels ? ch : (ch === colorChannels ? alphaTarget : -1);
|
|
1804
|
+
ch++;
|
|
1805
|
+
|
|
1806
|
+
if (target === -1) continue;
|
|
1807
|
+
|
|
1620
1808
|
const w = cright - cleft;
|
|
1621
1809
|
const h = cbottom - ctop;
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1810
|
+
const rowBytes = w * (depth / 8);
|
|
1811
|
+
const channelSize = rowBytes * h;
|
|
1624
1812
|
|
|
1625
1813
|
if (compressionMode === 0) {
|
|
1626
|
-
if (
|
|
1627
|
-
for (let y = 0; y < h; y++) {
|
|
1628
|
-
for (let x = 0; x < w; x++) {
|
|
1629
|
-
const src = x + y * w;
|
|
1630
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1631
|
-
data[dst + ch] = cdata[src];
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
} else if (colorMode === ColorMode.Grayscale && ch < 1) {
|
|
1635
|
-
for (let y = 0; y < h; y++) {
|
|
1636
|
-
for (let x = 0; x < w; x++) {
|
|
1637
|
-
const src = x + y * w;
|
|
1638
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1639
|
-
const value = cdata[src];
|
|
1640
|
-
data[dst + 0] = value;
|
|
1641
|
-
data[dst + 1] = value;
|
|
1642
|
-
data[dst + 2] = value;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
} else if (colorMode === ColorMode.Indexed) {
|
|
1646
|
-
for (let y = 0; y < h; y++) {
|
|
1647
|
-
for (let x = 0; x < w; x++) {
|
|
1648
|
-
const src = x + y * w;
|
|
1649
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1650
|
-
const index = cdata[src];
|
|
1651
|
-
const color = palette[index];
|
|
1652
|
-
data[dst + 0] = color.r;
|
|
1653
|
-
data[dst + 1] = color.g;
|
|
1654
|
-
data[dst + 2] = color.b;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
} else {
|
|
1658
|
-
if (reader.throwForMissingFeatures) throw new Error('Invalid color pattern');
|
|
1659
|
-
}
|
|
1814
|
+
if (cdata.byteLength < channelSize) throw new Error('Pattern channel data out of bounds');
|
|
1660
1815
|
} else if (compressionMode === 1) {
|
|
1661
|
-
|
|
1816
|
+
// each 2 bytes of RLE data can produce at most 128 bytes
|
|
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
|
+
}
|
|
1662
1825
|
|
|
1663
|
-
|
|
1664
|
-
|
|
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
|
+
}
|
|
1838
|
+
|
|
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) {
|
|
1846
|
+
bytes = new Uint8Array(channelSize);
|
|
1665
1847
|
const cdataReader = createReader(cdata.buffer, cdata.byteOffset, cdata.byteLength);
|
|
1848
|
+
readDataRLE(cdataReader, { data: bytes, width: rowBytes, height: h }, rowBytes, h, 8, 1, [0], false);
|
|
1849
|
+
}
|
|
1666
1850
|
|
|
1667
|
-
|
|
1668
|
-
readDataRLE(cdataReader, tempData, w, h, 8, 1, [0], false);
|
|
1669
|
-
copyChannelToRGBA(tempData, pixelData, ox, oy, ch);
|
|
1670
|
-
}
|
|
1851
|
+
if (depth !== 8) bytes = patternChannelTo8bit(bytes, w * h, depth);
|
|
1671
1852
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
copyChannelToRGBA(tempData, pixelData, ox, oy, 0);
|
|
1675
|
-
setupGrayscale(pixelData);
|
|
1676
|
-
}
|
|
1853
|
+
const ox = cleft - left;
|
|
1854
|
+
const oy = ctop - top;
|
|
1677
1855
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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;
|
|
1681
1867
|
}
|
|
1682
1868
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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];
|
|
1881
|
+
data[dst + 0] = value;
|
|
1882
|
+
data[dst + 1] = value;
|
|
1883
|
+
data[dst + 2] = value;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1686
1886
|
}
|
|
1887
|
+
}
|
|
1687
1888
|
|
|
1688
|
-
|
|
1889
|
+
if (cmyk) {
|
|
1890
|
+
cmykToRgb({ width, height, data: cmyk }, { width, height, data }, false);
|
|
1689
1891
|
}
|
|
1690
1892
|
|
|
1893
|
+
consumeMemory(reader, size);
|
|
1691
1894
|
reader.offset = end;
|
|
1692
1895
|
|
|
1693
1896
|
return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data };
|
|
1694
1897
|
}
|
|
1695
1898
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
const
|
|
1699
|
-
const width = dstData.width;
|
|
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);
|
|
1700
1902
|
|
|
1701
|
-
|
|
1702
|
-
for (let
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1903
|
+
if (depth === 16) {
|
|
1904
|
+
for (let i = 0; i < count; i++) {
|
|
1905
|
+
result[i] = Math.round(((bytes[i * 2] << 8) | bytes[i * 2 + 1]) / 257);
|
|
1906
|
+
}
|
|
1907
|
+
} else {
|
|
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];
|
|
1707
1924
|
}
|
|
1708
1925
|
}
|
|
1709
1926
|
}
|
|
1710
1927
|
|
|
1711
|
-
function consumeMemory(reader: PsdReader, size: number) {
|
|
1928
|
+
export function consumeMemory(reader: PsdReader, size: number) {
|
|
1929
|
+
if (!(size >= 0)) throw new Error('Invalid size');
|
|
1930
|
+
|
|
1712
1931
|
if (reader.totalMemoryLimit !== undefined) {
|
|
1713
1932
|
if (reader.totalMemoryLimit < size) throw new Error('Exceeded memory limit');
|
|
1714
1933
|
reader.totalMemoryLimit -= size;
|