ag-psd 31.0.3 → 33.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/00d0e0b4 +0 -0
- 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/0cc76730 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/1130885f +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/20729831 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/214a593b +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/226e2801 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/25f8b84a +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/27518289 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/2a19133e +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/330ef2d9 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/37ff16b8 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/3f72a97c +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/75efccd4 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/762f9305 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/7bcfc50a +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/7ea06cea +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/825e9a05 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/87372817 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/87cf2209 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/891a7c73 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/8b68a3a2 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/932f0770 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/a1d12a93 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/a7e80865 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/ac317c9c +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/b8569d6b +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/b8b073c4 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/bb489c89 +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/c46725e1 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/c794cb34 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/cd4ae980 +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/d02dd6f7 +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/e6735171 +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/f5417276 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/f6fd8125 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/f74d3518 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/f886deac +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/fd0a2987 +0 -0
- package/.v8-cache/v24.7.0-x64-ef5a0af0/fe349423 +0 -0
- package/CHANGELOG.md +107 -0
- package/LICENSE +5 -0
- package/README.md +3 -6
- package/README_PSD.md +242 -81
- package/dist/abr.d.ts +11 -4
- package/dist/abr.js +363 -229
- package/dist/abr.js.map +1 -1
- package/dist/additionalInfo.d.ts +56 -2
- package/dist/additionalInfo.js +600 -364
- 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 +3096 -1541
- package/dist/descriptor.d.ts +25 -6
- package/dist/descriptor.js +380 -135
- package/dist/descriptor.js.map +1 -1
- package/dist/effectsHelpers.js +12 -12
- package/dist/effectsHelpers.js.map +1 -1
- package/dist/engineData.d.ts +3 -1
- package/dist/engineData.js +34 -13
- package/dist/engineData.js.map +1 -1
- package/dist/engineData2.js +50 -21
- package/dist/engineData2.js.map +1 -1
- package/dist/helpers.d.ts +6 -3
- package/dist/helpers.js +22 -6
- package/dist/helpers.js.map +1 -1
- package/dist/imageResources.d.ts +15 -3
- package/dist/imageResources.js +529 -144
- package/dist/imageResources.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jpeg.js +30 -10
- package/dist/jpeg.js.map +1 -1
- package/dist/psd.d.ts +154 -18
- package/dist/psd.js.map +1 -1
- package/dist/psdReader.d.ts +12 -2
- package/dist/psdReader.js +720 -421
- package/dist/psdReader.js.map +1 -1
- package/dist/psdWriter.d.ts +3 -0
- package/dist/psdWriter.js +167 -130
- package/dist/psdWriter.js.map +1 -1
- package/dist/text.js +143 -29
- package/dist/text.js.map +1 -1
- package/dist/utf8.js +36 -18
- package/dist/utf8.js.map +1 -1
- package/dist-es/abr.d.ts +11 -4
- package/dist-es/abr.js +363 -229
- package/dist-es/abr.js.map +1 -1
- package/dist-es/additionalInfo.d.ts +56 -2
- package/dist-es/additionalInfo.js +600 -364
- 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 +25 -6
- package/dist-es/descriptor.js +380 -135
- package/dist-es/descriptor.js.map +1 -1
- package/dist-es/effectsHelpers.js +12 -12
- package/dist-es/effectsHelpers.js.map +1 -1
- package/dist-es/engineData.d.ts +3 -1
- package/dist-es/engineData.js +34 -13
- package/dist-es/engineData.js.map +1 -1
- package/dist-es/engineData2.js +50 -21
- package/dist-es/engineData2.js.map +1 -1
- package/dist-es/helpers.d.ts +6 -3
- package/dist-es/helpers.js +22 -6
- package/dist-es/helpers.js.map +1 -1
- package/dist-es/imageResources.d.ts +15 -3
- package/dist-es/imageResources.js +529 -144
- package/dist-es/imageResources.js.map +1 -1
- package/dist-es/index.js +1 -0
- package/dist-es/index.js.map +1 -1
- package/dist-es/jpeg.js +30 -10
- package/dist-es/jpeg.js.map +1 -1
- package/dist-es/psd.d.ts +154 -18
- package/dist-es/psd.js.map +1 -1
- package/dist-es/psdReader.d.ts +12 -2
- package/dist-es/psdReader.js +720 -421
- package/dist-es/psdReader.js.map +1 -1
- package/dist-es/psdWriter.d.ts +3 -0
- package/dist-es/psdWriter.js +167 -130
- package/dist-es/psdWriter.js.map +1 -1
- package/dist-es/text.js +143 -29
- package/dist-es/text.js.map +1 -1
- package/dist-es/utf8.js +36 -18
- package/dist-es/utf8.js.map +1 -1
- package/package.json +2 -2
- package/src/abr.ts +380 -232
- package/src/additionalInfo.ts +781 -477
- package/src/ase.ts +66 -61
- package/src/descriptor.ts +386 -142
- package/src/effectsHelpers.ts +9 -9
- package/src/engineData.ts +36 -13
- package/src/engineData2.ts +468 -432
- package/src/helpers.ts +23 -5
- package/src/imageResources.ts +658 -167
- package/src/index.ts +1 -0
- package/src/jpeg.ts +27 -12
- package/src/psd.ts +158 -30
- package/src/psdReader.ts +732 -414
- package/src/psdWriter.ts +186 -132
- package/src/text.ts +139 -18
- package/src/utf8.ts +35 -21
package/src/psdReader.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
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 } 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 {
|
|
@@ -99,20 +137,22 @@ export function readFixedPointPath32(reader: PsdReader): number {
|
|
|
99
137
|
return readInt32(reader) / (1 << 24);
|
|
100
138
|
}
|
|
101
139
|
|
|
140
|
+
// NOTE: returned array may alias the reader's underlying buffer, never write into it
|
|
102
141
|
export function readBytes(reader: PsdReader, length: number) {
|
|
103
|
-
const start = reader.
|
|
142
|
+
const start = reader.offset;
|
|
143
|
+
const viewLength = reader.view.byteLength;
|
|
104
144
|
reader.offset += length;
|
|
105
145
|
|
|
106
|
-
if ((start + length) >
|
|
146
|
+
if ((start + length) > viewLength) {
|
|
107
147
|
// fix for broken PSD files that are missing part of file at the end
|
|
108
148
|
warnOrThrow(reader, 'Reading bytes exceeding buffer length');
|
|
109
149
|
if (length > (100 * 1024 * 1024)) throw new Error('Reading past end of file'); // limit to 100MB
|
|
110
150
|
const result = new Uint8Array(length);
|
|
111
|
-
const len = Math.min(length,
|
|
112
|
-
if (len > 0) result.set(new Uint8Array(reader.view.buffer, start, len));
|
|
151
|
+
const len = Math.min(length, viewLength - start);
|
|
152
|
+
if (len > 0) result.set(new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, len));
|
|
113
153
|
return result;
|
|
114
154
|
} else {
|
|
115
|
-
return new Uint8Array(reader.view.buffer, start, length);
|
|
155
|
+
return new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, length);
|
|
116
156
|
}
|
|
117
157
|
}
|
|
118
158
|
|
|
@@ -144,41 +184,65 @@ export function readUnicodeString(reader: PsdReader) {
|
|
|
144
184
|
return readUnicodeStringWithLength(reader, length);
|
|
145
185
|
}
|
|
146
186
|
|
|
147
|
-
|
|
187
|
+
const STRING_CHUNK_SIZE = 0x2000;
|
|
188
|
+
|
|
189
|
+
function checkReadBounds(reader: PsdReader, size: number) {
|
|
190
|
+
if (reader.offset < 0 || (reader.offset + size) > reader.view.byteLength) {
|
|
191
|
+
throw new RangeError('Offset is outside the bounds of the DataView');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readUtf16String(reader: PsdReader, length: number, littleEndian: boolean) {
|
|
196
|
+
if (!(length > 0)) return '';
|
|
197
|
+
|
|
198
|
+
checkReadBounds(reader, length * 2);
|
|
199
|
+
|
|
200
|
+
const view = reader.view;
|
|
201
|
+
const codes = new Uint16Array(Math.min(length, STRING_CHUNK_SIZE));
|
|
202
|
+
let offset = reader.offset;
|
|
148
203
|
let text = '';
|
|
149
204
|
|
|
150
|
-
|
|
151
|
-
const
|
|
205
|
+
for (let i = 0; i < length; i += STRING_CHUNK_SIZE) {
|
|
206
|
+
const count = Math.min(STRING_CHUNK_SIZE, length - i);
|
|
152
207
|
|
|
153
|
-
|
|
154
|
-
|
|
208
|
+
for (let j = 0; j < count; j++, offset += 2) {
|
|
209
|
+
codes[j] = view.getUint16(offset, littleEndian);
|
|
155
210
|
}
|
|
211
|
+
|
|
212
|
+
text += String.fromCharCode.apply(null, (count === codes.length ? codes : codes.subarray(0, count)) as any);
|
|
156
213
|
}
|
|
157
214
|
|
|
215
|
+
reader.offset = offset;
|
|
216
|
+
|
|
217
|
+
// remove trailing \0
|
|
218
|
+
if (text.charCodeAt(text.length - 1) === 0) text = text.substring(0, text.length - 1);
|
|
219
|
+
|
|
158
220
|
return text;
|
|
159
221
|
}
|
|
160
222
|
|
|
161
|
-
|
|
223
|
+
function bytesToString(bytes: Uint8Array) {
|
|
162
224
|
let text = '';
|
|
163
225
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (value || length > 0) { // remove trailing \0
|
|
168
|
-
text += String.fromCharCode(value);
|
|
169
|
-
}
|
|
226
|
+
for (let i = 0; i < bytes.length; i += STRING_CHUNK_SIZE) {
|
|
227
|
+
text += String.fromCharCode.apply(null, bytes.subarray(i, i + STRING_CHUNK_SIZE) as any);
|
|
170
228
|
}
|
|
171
229
|
|
|
172
230
|
return text;
|
|
173
231
|
}
|
|
174
232
|
|
|
175
|
-
export function
|
|
176
|
-
|
|
233
|
+
export function readUnicodeStringWithLength(reader: PsdReader, length: number) {
|
|
234
|
+
return readUtf16String(reader, length, false);
|
|
235
|
+
}
|
|
177
236
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
237
|
+
export function readUnicodeStringWithLengthLE(reader: PsdReader, length: number) {
|
|
238
|
+
return readUtf16String(reader, length, true);
|
|
239
|
+
}
|
|
181
240
|
|
|
241
|
+
export function readAsciiString(reader: PsdReader, length: number) {
|
|
242
|
+
if (!(length > 0)) return '';
|
|
243
|
+
checkReadBounds(reader, length);
|
|
244
|
+
const text = bytesToString(new Uint8Array(reader.view.buffer, reader.view.byteOffset + reader.offset, length));
|
|
245
|
+
reader.offset += length;
|
|
182
246
|
return text;
|
|
183
247
|
}
|
|
184
248
|
|
|
@@ -196,14 +260,7 @@ export function checkSignature(reader: PsdReader, a: string, b?: string) {
|
|
|
196
260
|
}
|
|
197
261
|
|
|
198
262
|
function readShortString(reader: PsdReader, length: number) {
|
|
199
|
-
|
|
200
|
-
let result = '';
|
|
201
|
-
|
|
202
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
203
|
-
result += String.fromCharCode(buffer[i]);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return result;
|
|
263
|
+
return bytesToString(readBytes(reader, length));
|
|
207
264
|
}
|
|
208
265
|
|
|
209
266
|
function isValidSignature(sig: string) {
|
|
@@ -225,7 +282,8 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
225
282
|
const maxSize = version === 1 ? 30000 : 300000;
|
|
226
283
|
|
|
227
284
|
if (width > maxSize || height > maxSize) throw new Error(`Invalid size: ${width}x${height}`);
|
|
228
|
-
|
|
285
|
+
// up to 56 channels plus transparency of composite image (Photoshop CS3 writes 57 channels for 53 alpha channels)
|
|
286
|
+
if (channels < 1 || channels > 57) throw new Error(`Invalid channel count: ${channels}`);
|
|
229
287
|
if (![1, 8, 16, 32].includes(bitsPerChannel)) throw new Error(`Invalid bitsPerChannel: ${bitsPerChannel}`);
|
|
230
288
|
if (supportedColorModes.indexOf(colorMode) === -1) throw new Error(`Color mode not supported: ${colorModes[colorMode] ?? colorMode}`);
|
|
231
289
|
|
|
@@ -234,6 +292,7 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
234
292
|
Object.assign(reader, readOptions);
|
|
235
293
|
reader.large = version === 2;
|
|
236
294
|
reader.globalAlpha = false;
|
|
295
|
+
reader.layersInGlobalInfo = false;
|
|
237
296
|
if (!('totalMemoryLimit' in reader)) { // setting totalMemoryLimit to undefined explicitly disables memory limit
|
|
238
297
|
reader.totalMemoryLimit = 2 * 1024 * 1024 * 1024; // default 2GB memory limit
|
|
239
298
|
}
|
|
@@ -261,12 +320,16 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
261
320
|
// image resources
|
|
262
321
|
|
|
263
322
|
const imageResources: InternalImageResources = {};
|
|
323
|
+
let unreadResourceBytes = 0;
|
|
264
324
|
|
|
265
325
|
readSection(reader, 1, left => {
|
|
266
|
-
|
|
267
|
-
|
|
326
|
+
// stop if there's not enough data left for resource header (some files have few trailing bytes in this section)
|
|
327
|
+
while (left() >= 12) {
|
|
328
|
+
const signature = realignWithSignature(reader, isValidSignature);
|
|
268
329
|
const id = readUint16(reader);
|
|
269
|
-
|
|
330
|
+
const nameLength = readUint8(reader);
|
|
331
|
+
const name = readBytes(reader, nameLength);
|
|
332
|
+
skipBytes(reader, (nameLength + 1) % 2); // pad to even size
|
|
270
333
|
|
|
271
334
|
readSection(reader, 2, left => {
|
|
272
335
|
const handler = resourceHandlersMap[id];
|
|
@@ -274,20 +337,29 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
274
337
|
|
|
275
338
|
if (handler && !skip) {
|
|
276
339
|
try {
|
|
277
|
-
handler.read(reader, imageResources, left);
|
|
340
|
+
handler.read(reader, imageResources, left, { signature, id, name, width, height });
|
|
278
341
|
} catch (e) {
|
|
279
342
|
if (reader.throwForMissingFeatures) throw e;
|
|
280
343
|
skipBytes(reader, left());
|
|
281
344
|
}
|
|
282
345
|
} else {
|
|
283
|
-
|
|
346
|
+
reader.logMissingFeatures && reader.log(`Unhandled image resource: ${id} (${left()})`);
|
|
284
347
|
skipBytes(reader, left());
|
|
285
348
|
}
|
|
286
349
|
});
|
|
287
350
|
}
|
|
351
|
+
|
|
352
|
+
unreadResourceBytes = left();
|
|
353
|
+
skipBytes(reader, left());
|
|
288
354
|
});
|
|
289
355
|
|
|
290
|
-
|
|
356
|
+
// some files have image resources section length few bytes larger than the data, layer and mask info section
|
|
357
|
+
// starts right after the last resource in that case
|
|
358
|
+
if (unreadResourceBytes && !sectionFits(reader, reader.offset) && sectionFits(reader, reader.offset - unreadResourceBytes)) {
|
|
359
|
+
reader.offset -= unreadResourceBytes;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { layersGroup, layerGroupsEnabledId, savedPathsOrigination, ...rest } = imageResources;
|
|
291
363
|
|
|
292
364
|
if (Object.keys(rest).length) {
|
|
293
365
|
psd.imageResources = rest;
|
|
@@ -331,7 +403,11 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
331
403
|
|
|
332
404
|
if (!skipComposite) {
|
|
333
405
|
if (reader.useRawData) {
|
|
334
|
-
|
|
406
|
+
const rawOffset = Math.min(reader.offset, reader.view.byteLength);
|
|
407
|
+
const rawCompositeData = new Uint8Array(reader.view.buffer, reader.view.byteOffset + rawOffset, reader.view.byteLength - rawOffset);
|
|
408
|
+
psd.rawCompositeData = rawCompositeData;
|
|
409
|
+
psd.rawCompositeDataInfo = { large: reader.large, globalAlpha: reader.globalAlpha };
|
|
410
|
+
if (reader.layersInGlobalInfo) psd.rawCompositeDataInfo.layersInGlobalInfo = true;
|
|
335
411
|
} else {
|
|
336
412
|
const imageData = readImageData(reader, psd);
|
|
337
413
|
if (reader.useImageData) {
|
|
@@ -342,10 +418,6 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) {
|
|
|
342
418
|
}
|
|
343
419
|
}
|
|
344
420
|
|
|
345
|
-
// TODO: show converted color mode instead of original PSD file color mode
|
|
346
|
-
// but add option to preserve file color mode (need to return image data instead of canvas in that case)
|
|
347
|
-
// psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB
|
|
348
|
-
|
|
349
421
|
return psd;
|
|
350
422
|
}
|
|
351
423
|
|
|
@@ -393,7 +465,8 @@ export function readLayerInfo(reader: PsdReader, psd: Psd, imageResources: Inter
|
|
|
393
465
|
stack[stack.length - 1].children!.unshift(l);
|
|
394
466
|
stack.push(l);
|
|
395
467
|
} else if (type === SectionDividerType.BoundingSectionDivider) {
|
|
396
|
-
|
|
468
|
+
// ignore unmatched section dividers, never pop the root
|
|
469
|
+
if (stack.length > 1) stack.pop();
|
|
397
470
|
// this was workaround because I didn't know what `lsdk` section was, now it's probably not needed anymore
|
|
398
471
|
// } else if (l.name === '</Layer group>' && !l.sectionDivider && !l.top && !l.left && !l.bottom && !l.right) {
|
|
399
472
|
// // sometimes layer group terminator doesn't have sectionDivider, so we just guess here (PS bug ?)
|
|
@@ -429,8 +502,11 @@ function readLayerRecord(reader: PsdReader, psd: Psd, imageResources: InternalIm
|
|
|
429
502
|
|
|
430
503
|
checkSignature(reader, '8BIM');
|
|
431
504
|
const blendMode = readSignature(reader);
|
|
432
|
-
if (!toBlendMode[blendMode])
|
|
433
|
-
|
|
505
|
+
if (!toBlendMode[blendMode]) {
|
|
506
|
+
if (reader.throwForMissingFeatures) throw new Error(`Invalid blend mode: '${blendMode}'`);
|
|
507
|
+
reader.logMissingFeatures && reader.log(`Unknown blend mode: '${blendMode}', using normal blend mode`);
|
|
508
|
+
}
|
|
509
|
+
layer.blendMode = toBlendMode[blendMode] || 'normal';
|
|
434
510
|
|
|
435
511
|
layer.opacity = readUint8(reader) / 0xff;
|
|
436
512
|
layer.clipping = readUint8(reader) === 1;
|
|
@@ -464,11 +540,20 @@ function readLayerRecord(reader: PsdReader, psd: Psd, imageResources: InternalIm
|
|
|
464
540
|
return { layer, channels };
|
|
465
541
|
}
|
|
466
542
|
|
|
543
|
+
// negative sizes are treated as empty (Photoshop writes them for some empty layers, like 0,0,-1,0 bounds of fill layer)
|
|
544
|
+
// checks if length of section (layer and mask info) at given offset fits in the file
|
|
545
|
+
function sectionFits(reader: PsdReader, offset: number) {
|
|
546
|
+
const lengthSize = reader.large ? 8 : 4;
|
|
547
|
+
if (offset < 0 || offset + lengthSize > reader.view.byteLength) return false;
|
|
548
|
+
const length = reader.large ? reader.view.getUint32(offset) * 0x100000000 + reader.view.getUint32(offset + 4) : reader.view.getUint32(offset);
|
|
549
|
+
return offset + lengthSize + length <= reader.view.byteLength;
|
|
550
|
+
}
|
|
551
|
+
|
|
467
552
|
function isValidBoxSize(box: { top?: number; left?: number; bottom?: number; right?: number; }, reader: PsdReader) {
|
|
468
553
|
const width = (box.right || 0) - (box.left || 0);
|
|
469
554
|
const height = (box.bottom || 0) - (box.top || 0);
|
|
470
555
|
const maxSize = reader.large ? 300000 : 30000;
|
|
471
|
-
return width
|
|
556
|
+
return width <= maxSize && height <= maxSize;
|
|
472
557
|
}
|
|
473
558
|
|
|
474
559
|
function readLayerMaskData(reader: PsdReader, layer: Layer) {
|
|
@@ -491,7 +576,7 @@ function readLayerMaskData(reader: PsdReader, layer: Layer) {
|
|
|
491
576
|
mask.disabled = (flags & LayerMaskFlags.LayerMaskDisabled) !== 0;
|
|
492
577
|
mask.fromVectorData = (flags & LayerMaskFlags.LayerMaskFromRenderingOtherData) !== 0;
|
|
493
578
|
|
|
494
|
-
if (
|
|
579
|
+
if (hasRealMaskData(reader, flags, left())) {
|
|
495
580
|
const realMask: LayerMaskData = {};
|
|
496
581
|
layer.realMask = realMask;
|
|
497
582
|
|
|
@@ -520,6 +605,44 @@ function readLayerMaskData(reader: PsdReader, layer: Layer) {
|
|
|
520
605
|
});
|
|
521
606
|
}
|
|
522
607
|
|
|
608
|
+
function maskParamsSize(params: number) {
|
|
609
|
+
return 1 +
|
|
610
|
+
((params & MaskParams.UserMaskDensity) ? 1 : 0) +
|
|
611
|
+
((params & MaskParams.UserMaskFeather) ? 8 : 0) +
|
|
612
|
+
((params & MaskParams.VectorMaskDensity) ? 1 : 0) +
|
|
613
|
+
((params & MaskParams.VectorMaskFeather) ? 8 : 0);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// checks if data at offset is mask parameters followed by up to 3 bytes of zero padding, ending exactly at offset + left
|
|
617
|
+
function isMaskParamsLayout(view: DataView, offset: number, left: number) {
|
|
618
|
+
if (left < 1) return false;
|
|
619
|
+
|
|
620
|
+
const params = view.getUint8(offset);
|
|
621
|
+
if (params === 0 || (params & ~0x0f)) return false; // only 4 parameter flags exist
|
|
622
|
+
|
|
623
|
+
const size = maskParamsSize(params);
|
|
624
|
+
if (left < size || left - size > 3) return false;
|
|
625
|
+
|
|
626
|
+
for (let i = size; i < left; i++) {
|
|
627
|
+
if (view.getUint8(offset + i) !== 0) return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// real mask data (18 bytes) is followed by mask parameters (if flag is set) and padding
|
|
634
|
+
function hasRealMaskData(reader: PsdReader, flags: number, left: number) {
|
|
635
|
+
if (left < 18) return false;
|
|
636
|
+
if (!(flags & LayerMaskFlags.MaskHasParametersAppliedToIt)) return true;
|
|
637
|
+
|
|
638
|
+
const { view, offset } = reader;
|
|
639
|
+
if (isMaskParamsLayout(view, offset, left)) return false;
|
|
640
|
+
if (isMaskParamsLayout(view, offset + 18, left - 18)) return true;
|
|
641
|
+
|
|
642
|
+
// unknown layout, assume real mask if there's enough data for it (and mask parameters)
|
|
643
|
+
return left === 18 || left - 18 >= maskParamsSize(view.getUint8(offset + 18));
|
|
644
|
+
}
|
|
645
|
+
|
|
523
646
|
function readBlendingRange(reader: PsdReader) {
|
|
524
647
|
return [readUint8(reader), readUint8(reader), readUint8(reader), readUint8(reader)];
|
|
525
648
|
}
|
|
@@ -561,9 +684,9 @@ function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, ch
|
|
|
561
684
|
compression = readUint16(reader) as Compression;
|
|
562
685
|
}
|
|
563
686
|
|
|
564
|
-
// try to fix broken files where there's 1 byte shift of channel
|
|
687
|
+
// try to fix broken files where there's 1 byte shift of channel (in the other direction)
|
|
565
688
|
if (compression > 3) {
|
|
566
|
-
reader.offset -=
|
|
689
|
+
reader.offset -= 4;
|
|
567
690
|
compression = readUint16(reader) as Compression;
|
|
568
691
|
}
|
|
569
692
|
|
|
@@ -599,8 +722,15 @@ export function getCompositeImageData(psd: Psd) {
|
|
|
599
722
|
if (!data) return undefined;
|
|
600
723
|
|
|
601
724
|
const reader = createReader(data.buffer, data.byteOffset, data.byteLength);
|
|
602
|
-
const
|
|
603
|
-
|
|
725
|
+
const info = psd.rawCompositeDataInfo;
|
|
726
|
+
|
|
727
|
+
if (info) {
|
|
728
|
+
reader.large = info.large;
|
|
729
|
+
reader.globalAlpha = info.globalAlpha;
|
|
730
|
+
reader.layersInGlobalInfo = !!info.layersInGlobalInfo;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return readImageData(reader, psd);
|
|
604
734
|
}
|
|
605
735
|
|
|
606
736
|
export function getCompositeCanvas(psd: Psd) {
|
|
@@ -649,21 +779,23 @@ export function decodeLayerPixels(layer: Layer, useImageData?: boolean) {
|
|
|
649
779
|
decodeLayerImageData(layer, { useImageData });
|
|
650
780
|
}
|
|
651
781
|
|
|
652
|
-
|
|
782
|
+
type LogOptions = Pick<ReadOptions, 'debug' | 'log'>;
|
|
783
|
+
|
|
784
|
+
function decodeLayerImageData(layer: Layer, options: Pick<ReadOptions, 'useImageData' | 'throwForMissingFeatures' | 'totalMemoryLimit' | 'debug' | 'log'>) {
|
|
653
785
|
let { throwForMissingFeatures, useImageData } = options;
|
|
654
786
|
|
|
655
|
-
const imageData = getDataFromLayer(layer, LayerDataType.Layer, throwForMissingFeatures, options.totalMemoryLimit);
|
|
787
|
+
const imageData = getDataFromLayer(layer, LayerDataType.Layer, throwForMissingFeatures, options.totalMemoryLimit, options);
|
|
656
788
|
setImageDataOrCanvas(layer, imageData, useImageData);
|
|
657
789
|
if (options.totalMemoryLimit !== undefined) options.totalMemoryLimit -= imageData?.data.byteLength ?? 0;
|
|
658
790
|
|
|
659
791
|
if (layer.mask) {
|
|
660
|
-
const maskData = getDataFromLayer(layer, LayerDataType.Mask, throwForMissingFeatures, options.totalMemoryLimit);
|
|
792
|
+
const maskData = getDataFromLayer(layer, LayerDataType.Mask, throwForMissingFeatures, options.totalMemoryLimit, options);
|
|
661
793
|
setImageDataOrCanvas(layer.mask, maskData, useImageData);
|
|
662
794
|
if (options.totalMemoryLimit !== undefined) options.totalMemoryLimit -= maskData?.data.byteLength ?? 0;
|
|
663
795
|
}
|
|
664
796
|
|
|
665
797
|
if (layer.realMask) {
|
|
666
|
-
const maskData = getDataFromLayer(layer, LayerDataType.RealMask, throwForMissingFeatures, options.totalMemoryLimit);
|
|
798
|
+
const maskData = getDataFromLayer(layer, LayerDataType.RealMask, throwForMissingFeatures, options.totalMemoryLimit, options);
|
|
667
799
|
setImageDataOrCanvas(layer.realMask, maskData, useImageData);
|
|
668
800
|
if (options.totalMemoryLimit !== undefined) options.totalMemoryLimit -= maskData?.data.byteLength ?? 0;
|
|
669
801
|
}
|
|
@@ -677,7 +809,7 @@ enum LayerDataType {
|
|
|
677
809
|
RealMask,
|
|
678
810
|
}
|
|
679
811
|
|
|
680
|
-
function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeatures?: boolean, memoryLimit?: number) {
|
|
812
|
+
function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeatures?: boolean, memoryLimit?: number, logOptions?: LogOptions) {
|
|
681
813
|
if (!layer.rawData) return undefined;
|
|
682
814
|
|
|
683
815
|
const { colorMode, bitsPerChannel, channels, large } = layer.rawData;
|
|
@@ -691,22 +823,20 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
691
823
|
|
|
692
824
|
if (layerWidth && layerHeight && read === LayerDataType.Layer) {
|
|
693
825
|
if (cmyk) {
|
|
694
|
-
if (bitsPerChannel !== 8) throw new Error('bitsPerChannel
|
|
695
|
-
imageData =
|
|
826
|
+
if (bitsPerChannel !== 8 && bitsPerChannel !== 16) throw new Error('bitsPerChannel not supported');
|
|
827
|
+
imageData = createImageDataBitDepth(layerWidth, layerHeight, bitsPerChannel, 5, memoryLimit);
|
|
696
828
|
} else {
|
|
697
829
|
imageData = createImageDataBitDepth(layerWidth, layerHeight, bitsPerChannel, 4, memoryLimit);
|
|
698
830
|
}
|
|
699
831
|
}
|
|
700
832
|
|
|
701
|
-
if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
|
|
702
|
-
(layer as any).imageDataRaw = [];
|
|
703
|
-
(layer as any).imageDataRawCompression = [];
|
|
704
|
-
}
|
|
705
|
-
|
|
706
833
|
for (const { id, compression, data } of channels) {
|
|
707
834
|
if (!data) continue;
|
|
708
835
|
|
|
709
836
|
const dataReader = createReader(data.buffer, data.byteOffset, data.byteLength);
|
|
837
|
+
dataReader.debug = !!logOptions?.debug;
|
|
838
|
+
if (logOptions?.log) dataReader.log = logOptions.log;
|
|
839
|
+
if (memoryLimit !== undefined) dataReader.totalMemoryLimit = Math.max(0, memoryLimit - (imageData?.data.byteLength ?? 0));
|
|
710
840
|
|
|
711
841
|
if (id === ChannelID.UserMask || id === ChannelID.RealUserMask) {
|
|
712
842
|
if (id === ChannelID.UserMask && read !== LayerDataType.Mask) continue;
|
|
@@ -720,16 +850,8 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
720
850
|
|
|
721
851
|
if (maskWidth && maskHeight) {
|
|
722
852
|
maskData = createImageDataBitDepth(maskWidth, maskHeight, bitsPerChannel, 4, memoryLimit);
|
|
853
|
+
if (memoryLimit !== undefined) dataReader.totalMemoryLimit = Math.max(0, dataReader.totalMemoryLimit! - maskData.data.byteLength);
|
|
723
854
|
readData(dataReader, data.byteLength, maskData, compression, maskWidth, maskHeight, bitsPerChannel, 0, large, 4);
|
|
724
|
-
if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
|
|
725
|
-
if (id === ChannelID.UserMask) {
|
|
726
|
-
(layer as any).maskDataRawCompression = compression;
|
|
727
|
-
(layer as any).maskDataRaw = data;
|
|
728
|
-
} else {
|
|
729
|
-
(layer as any).realMaskDataRawCompression = compression;
|
|
730
|
-
(layer as any).realMaskDataRaw = data;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
855
|
setupGrayscale(maskData);
|
|
734
856
|
resetAlpha(maskData, false);
|
|
735
857
|
}
|
|
@@ -748,15 +870,6 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
748
870
|
}
|
|
749
871
|
|
|
750
872
|
readData(dataReader, data.byteLength, targetData, compression, layerWidth, layerHeight, bitsPerChannel, offset, large, cmyk ? 5 : 4);
|
|
751
|
-
|
|
752
|
-
if (RAW_IMAGE_DATA) { // TODO: use layer.rawData instead
|
|
753
|
-
(layer as any).imageDataRawCompression[id] = compression;
|
|
754
|
-
(layer as any).imageDataRaw[id] = data;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (targetData && colorMode === ColorMode.Grayscale) {
|
|
758
|
-
setupGrayscale(targetData);
|
|
759
|
-
}
|
|
760
873
|
}
|
|
761
874
|
|
|
762
875
|
if (id === ChannelID.Transparency) {
|
|
@@ -765,11 +878,13 @@ function getDataFromLayer(layer: Layer, read: LayerDataType, throwForMissingFeat
|
|
|
765
878
|
}
|
|
766
879
|
|
|
767
880
|
if (imageData) {
|
|
881
|
+
if (colorMode === ColorMode.Grayscale) setupGrayscale(imageData);
|
|
768
882
|
if (!initializedAlpha) resetAlpha(imageData, cmyk);
|
|
769
883
|
|
|
770
884
|
if (cmyk) {
|
|
771
885
|
const cmykData = imageData;
|
|
772
|
-
|
|
886
|
+
const rgbMemoryLimit = memoryLimit === undefined ? undefined : Math.max(0, memoryLimit - cmykData.data.byteLength);
|
|
887
|
+
imageData = createImageDataBitDepth(cmykData.width, cmykData.height, bitsPerChannel, 4, rgbMemoryLimit);
|
|
773
888
|
cmykToRgb(cmykData, imageData, false);
|
|
774
889
|
}
|
|
775
890
|
}
|
|
@@ -782,7 +897,7 @@ function readData(reader: PsdReader, length: number, pixels: PixelData | undefin
|
|
|
782
897
|
|
|
783
898
|
if (compression === Compression.RawData) {
|
|
784
899
|
if (length !== (width * height * Math.floor(bitDepth / 8))) {
|
|
785
|
-
reader
|
|
900
|
+
warnOrThrow(reader, `Invalid length (${length}, ${width * height * Math.floor(bitDepth / 8)})`);
|
|
786
901
|
}
|
|
787
902
|
const data = readBytes(reader, length);
|
|
788
903
|
readDataRaw(data, pixels, bitDepth, step, offset);
|
|
@@ -791,10 +906,10 @@ function readData(reader: PsdReader, length: number, pixels: PixelData | undefin
|
|
|
791
906
|
readDataRLE(reader, pixels, width, height, bitDepth, step, [offset], large);
|
|
792
907
|
} else if (compression === Compression.ZipWithoutPrediction) {
|
|
793
908
|
const data = readBytes(reader, length);
|
|
794
|
-
readDataZip(data, pixels, width, height, bitDepth, step, offset, false);
|
|
909
|
+
readDataZip(data, pixels, width, height, bitDepth, step, offset, false, reader);
|
|
795
910
|
} else if (compression === Compression.ZipWithPrediction) {
|
|
796
911
|
const data = readBytes(reader, length);
|
|
797
|
-
readDataZip(data, pixels, width, height, bitDepth, step, offset, true);
|
|
912
|
+
readDataZip(data, pixels, width, height, bitDepth, step, offset, true, reader);
|
|
798
913
|
} else {
|
|
799
914
|
throw new Error(`Invalid Compression type: ${compression}`);
|
|
800
915
|
}
|
|
@@ -809,7 +924,7 @@ export function readGlobalLayerMaskInfo(reader: PsdReader) {
|
|
|
809
924
|
const colorSpace2 = readUint16(reader);
|
|
810
925
|
const colorSpace3 = readUint16(reader);
|
|
811
926
|
const colorSpace4 = readUint16(reader);
|
|
812
|
-
const opacity = readUint16(reader) /
|
|
927
|
+
const opacity = readUint16(reader) / 100; // 0 - 100
|
|
813
928
|
const kind = readUint8(reader);
|
|
814
929
|
skipBytes(reader, left()); // 3 bytes of padding ?
|
|
815
930
|
return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind };
|
|
@@ -847,7 +962,16 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition
|
|
|
847
962
|
const key = readSignature(reader);
|
|
848
963
|
|
|
849
964
|
// `largeAdditionalInfoKeys` fallback, because some keys don't have 8B64 signature even when they are 64bit
|
|
850
|
-
|
|
965
|
+
let u64 = sig === '8B64' || (reader.large && largeAdditionalInfoKeys.indexOf(key) !== -1);
|
|
966
|
+
|
|
967
|
+
// these keys were written by older versions of this library with 32bit length and 8BIM signature,
|
|
968
|
+
// 64bit length always has high 32bits set to 0 (lengths over 4GB are not supported)
|
|
969
|
+
if (reader.large && sig === '8BIM' && largeAdditionalInfoKeysObserved.indexOf(key) !== -1) {
|
|
970
|
+
// 32bit length of 0 is followed by signature of the next block, 64bit length would continue with its low 32bits
|
|
971
|
+
const next = reader.offset + 8 <= reader.view.byteLength ? reader.view.getUint32(reader.offset + 4) : 0;
|
|
972
|
+
const nextIsSignature = next === 0x3842494d /* 8BIM */ || next === 0x38423634 /* 8B64 */;
|
|
973
|
+
u64 = reader.view.getUint32(reader.offset) === 0 && !nextIsSignature;
|
|
974
|
+
}
|
|
851
975
|
|
|
852
976
|
readSection(reader, 2, left => {
|
|
853
977
|
const handler = infoHandlersMap[key];
|
|
@@ -859,7 +983,7 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition
|
|
|
859
983
|
if (reader.throwForMissingFeatures) throw e;
|
|
860
984
|
}
|
|
861
985
|
} else {
|
|
862
|
-
reader.logMissingFeatures && reader.log(`Unhandled additional info: ${key}`);
|
|
986
|
+
reader.logMissingFeatures && key !== 'GenI' && key !== 'OCIO' && key !== 'CAI ' && reader.log(`Unhandled additional info: ${key}`);
|
|
863
987
|
skipBytes(reader, left());
|
|
864
988
|
}
|
|
865
989
|
|
|
@@ -892,6 +1016,7 @@ function createImageDataBitDepth(width: number, height: number, bitDepth: number
|
|
|
892
1016
|
function readImageData(reader: PsdReader, psd: Psd) {
|
|
893
1017
|
const compression = readUint16(reader) as Compression;
|
|
894
1018
|
const bitsPerChannel = psd.bitsPerChannel ?? 8;
|
|
1019
|
+
const { width, height } = psd;
|
|
895
1020
|
|
|
896
1021
|
if (supportedColorModes.indexOf(psd.colorMode!) === -1)
|
|
897
1022
|
throw new Error(`Color mode not supported: ${psd.colorMode}`);
|
|
@@ -899,7 +1024,7 @@ function readImageData(reader: PsdReader, psd: Psd) {
|
|
|
899
1024
|
if (compression !== Compression.RawData && compression !== Compression.RleCompressed)
|
|
900
1025
|
throw new Error(`Compression type not supported: ${compression}`);
|
|
901
1026
|
|
|
902
|
-
const imageData = createImageDataBitDepth(
|
|
1027
|
+
const imageData = createImageDataBitDepth(width, height, bitsPerChannel, 4, reader.totalMemoryLimit);
|
|
903
1028
|
if (reader.totalMemoryLimit !== undefined) reader.totalMemoryLimit -= imageData.data.byteLength;
|
|
904
1029
|
resetImageData(imageData);
|
|
905
1030
|
|
|
@@ -907,45 +1032,24 @@ function readImageData(reader: PsdReader, psd: Psd) {
|
|
|
907
1032
|
case ColorMode.Bitmap: {
|
|
908
1033
|
if (bitsPerChannel !== 1) throw new Error('Invalid bitsPerChannel for bitmap color mode');
|
|
909
1034
|
|
|
1035
|
+
const rowBytes = Math.ceil(width / 8);
|
|
910
1036
|
let bytes: Uint8Array;
|
|
911
1037
|
|
|
912
1038
|
if (compression === Compression.RawData) {
|
|
913
|
-
bytes = readBytes(reader,
|
|
914
|
-
} else if (compression === Compression.RleCompressed) {
|
|
915
|
-
bytes = new Uint8Array(psd.width * psd.height);
|
|
916
|
-
readDataRLE(reader, { data: bytes, width: psd.width, height: psd.height }, psd.width, psd.height, 8, 1, [0], reader.large);
|
|
1039
|
+
bytes = readBytes(reader, rowBytes * height);
|
|
917
1040
|
} else {
|
|
918
|
-
|
|
1041
|
+
bytes = new Uint8Array(rowBytes * height);
|
|
1042
|
+
readDataRLE(reader, { data: bytes, width: rowBytes, height }, rowBytes, height, 8, 1, [0], reader.large);
|
|
919
1043
|
}
|
|
920
1044
|
|
|
921
|
-
decodeBitmap(bytes, imageData.data,
|
|
1045
|
+
decodeBitmap(bytes, imageData.data, width, height);
|
|
922
1046
|
break;
|
|
923
1047
|
}
|
|
924
1048
|
case ColorMode.RGB:
|
|
925
1049
|
case ColorMode.Grayscale: {
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
for (let i = 3; i < psd.channels; i++) {
|
|
930
|
-
// TODO: store these channels in additional image data
|
|
931
|
-
channels.push(i);
|
|
932
|
-
}
|
|
933
|
-
} else if (reader.globalAlpha) {
|
|
934
|
-
channels.push(3);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
if (compression === Compression.RawData) {
|
|
938
|
-
for (let i = 0; i < channels.length; i++) {
|
|
939
|
-
const data = readBytes(reader, psd.width * psd.height * Math.floor(bitsPerChannel / 8));
|
|
940
|
-
readDataRaw(data, imageData, bitsPerChannel, 4, channels[i]);
|
|
941
|
-
}
|
|
942
|
-
} else if (compression === Compression.RleCompressed) {
|
|
943
|
-
const start = reader.offset;
|
|
944
|
-
readDataRLE(reader, imageData, psd.width, psd.height, bitsPerChannel, 4, channels, reader.large);
|
|
945
|
-
if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
946
|
-
} else {
|
|
947
|
-
throw new Error(`Compression not supported: ${compression}`);
|
|
948
|
-
}
|
|
1050
|
+
const colorChannels = psd.colorMode === ColorMode.Grayscale ? 1 : 3;
|
|
1051
|
+
const offsets = compositeChannelOffsets(reader, psd, colorChannels, 3);
|
|
1052
|
+
readCompositeChannels(reader, compression, imageData, bitsPerChannel, 4, offsets);
|
|
949
1053
|
|
|
950
1054
|
if (psd.colorMode === ColorMode.Grayscale) {
|
|
951
1055
|
setupGrayscale(imageData);
|
|
@@ -953,303 +1057,492 @@ function readImageData(reader: PsdReader, psd: Psd) {
|
|
|
953
1057
|
break;
|
|
954
1058
|
}
|
|
955
1059
|
case ColorMode.Indexed: {
|
|
956
|
-
if (bitsPerChannel !== 8) throw new Error('bitsPerChannel
|
|
957
|
-
if (psd.channels !== 1) throw new Error('Invalid channel count');
|
|
1060
|
+
if (bitsPerChannel !== 8) throw new Error('bitsPerChannel not supported');
|
|
958
1061
|
if (!psd.palette) throw new Error('Missing color palette');
|
|
959
1062
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
width: imageData.width,
|
|
965
|
-
height: imageData.height,
|
|
966
|
-
data: new Uint8Array(imageData.width * imageData.height),
|
|
967
|
-
};
|
|
968
|
-
readDataRLE(reader, indexedImageData, psd.width, psd.height, bitsPerChannel, 1, [0], reader.large);
|
|
969
|
-
indexedToRgb(indexedImageData, imageData, psd.palette);
|
|
970
|
-
} else {
|
|
971
|
-
throw new Error(`Compression not supported: ${compression}`);
|
|
972
|
-
}
|
|
973
|
-
|
|
1063
|
+
const offsets = compositeChannelOffsets(reader, psd, 1, -1);
|
|
1064
|
+
const indexedImageData: PixelData = { width, height, data: new Uint8Array(width * height) };
|
|
1065
|
+
readCompositeChannels(reader, compression, indexedImageData, 8, 1, offsets);
|
|
1066
|
+
indexedToRgb(indexedImageData, imageData, psd.palette, psd.imageResources?.transparencyIndex ?? -1);
|
|
974
1067
|
break;
|
|
975
1068
|
}
|
|
976
1069
|
case ColorMode.CMYK: {
|
|
977
|
-
if (bitsPerChannel !== 8) throw new Error('bitsPerChannel
|
|
978
|
-
if (psd.channels !== 4) throw new Error(`Invalid channel count`);
|
|
979
|
-
|
|
980
|
-
const channels = [0, 1, 2, 3];
|
|
981
|
-
if (reader.globalAlpha) channels.push(4);
|
|
1070
|
+
if (bitsPerChannel !== 8 && bitsPerChannel !== 16) throw new Error('bitsPerChannel not supported');
|
|
982
1071
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
// TODO: ...
|
|
986
|
-
// for (let i = 0; i < channels.length; i++) {
|
|
987
|
-
// readDataRaw(reader, imageData, channels[i], psd.width, psd.height);
|
|
988
|
-
// }
|
|
989
|
-
} else if (compression === Compression.RleCompressed) {
|
|
990
|
-
const cmykImageData: PixelData = {
|
|
991
|
-
width: imageData.width,
|
|
992
|
-
height: imageData.height,
|
|
993
|
-
data: new Uint8Array(imageData.width * imageData.height * 5),
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
const start = reader.offset;
|
|
997
|
-
readDataRLE(reader, cmykImageData, psd.width, psd.height, bitsPerChannel, 5, channels, reader.large);
|
|
998
|
-
cmykToRgb(cmykImageData, imageData, true);
|
|
999
|
-
|
|
1000
|
-
if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
1001
|
-
} else {
|
|
1002
|
-
throw new Error(`Compression not supported: ${compression}`);
|
|
1003
|
-
}
|
|
1072
|
+
const offsets = compositeChannelOffsets(reader, psd, 4, 4);
|
|
1073
|
+
const cmykImageData = createImageDataBitDepth(width, height, bitsPerChannel, 5, reader.totalMemoryLimit);
|
|
1004
1074
|
|
|
1075
|
+
resetAlpha(cmykImageData, true);
|
|
1076
|
+
readCompositeChannels(reader, compression, cmykImageData, bitsPerChannel, 5, offsets);
|
|
1077
|
+
cmykToRgb(cmykImageData, imageData, !!reader.globalAlpha);
|
|
1005
1078
|
break;
|
|
1006
1079
|
}
|
|
1007
1080
|
default: throw new Error(`Color mode not supported: ${psd.colorMode}`);
|
|
1008
1081
|
}
|
|
1009
1082
|
|
|
1010
|
-
// remove weird white matte
|
|
1011
|
-
if (reader.globalAlpha) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1083
|
+
// remove weird white matte (already removed when converting from CMYK)
|
|
1084
|
+
if (reader.globalAlpha && psd.colorMode !== ColorMode.CMYK) {
|
|
1085
|
+
removeWhiteMatte(imageData);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return imageData;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// returns target offset for each of the composite image channels stored in the file, -1 for channels that are skipped
|
|
1092
|
+
function compositeChannelOffsets(reader: PsdReader, psd: Psd, colorChannels: number, alphaOffset: number) {
|
|
1093
|
+
const channelCount = psd.channels ?? (colorChannels + (reader.globalAlpha ? 1 : 0));
|
|
1094
|
+
if (channelCount < colorChannels) throw new Error(`Invalid channel count: ${channelCount}`);
|
|
1095
|
+
|
|
1096
|
+
// First extra channel is transparency if document has global alpha (negative layer count). Some non-Photoshop
|
|
1097
|
+
// applications write transparency channel without setting global alpha, in that case extra channel is not
|
|
1098
|
+
// listed in alpha channel names (Photoshop names all user alpha channels / saved selections).
|
|
1099
|
+
// Photoshop also lists merged transparency as first alpha channel named "Transparency" (in documents marked with
|
|
1100
|
+
// Mtrn/Mt16/Mt32 additional info, or in some 16bit documents with layers stored in Lr16 without any other mark)
|
|
1101
|
+
const alphaNames = psd.imageResources?.alphaChannelNames ?? [];
|
|
1102
|
+
const namedChannels = alphaNames.length;
|
|
1103
|
+
const namedTransparency = !!reader.layersInGlobalInfo && alphaNames[0] === 'Transparency' && namedChannels === channelCount - colorChannels;
|
|
1104
|
+
const hasTransparency = reader.globalAlpha || namedTransparency || (channelCount - colorChannels) > namedChannels;
|
|
1105
|
+
const offsets: number[] = [];
|
|
1106
|
+
|
|
1107
|
+
for (let i = 0; i < channelCount; i++) {
|
|
1108
|
+
if (i < colorChannels) {
|
|
1109
|
+
offsets.push(i);
|
|
1110
|
+
} else if (i === colorChannels && hasTransparency && alphaOffset >= 0) {
|
|
1111
|
+
offsets.push(alphaOffset);
|
|
1112
|
+
} else {
|
|
1113
|
+
// TODO: store these channels (alpha channels / saved selections, spot colors) in additional image data
|
|
1114
|
+
offsets.push(-1);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return offsets;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function readCompositeChannels(reader: PsdReader, compression: Compression, pixelData: PixelData, bitsPerChannel: number, step: number, offsets: number[]) {
|
|
1122
|
+
if (compression === Compression.RawData) {
|
|
1123
|
+
const channelSize = pixelData.width * pixelData.height * bytesPerValue(bitsPerChannel);
|
|
1124
|
+
|
|
1125
|
+
for (const offset of offsets) {
|
|
1126
|
+
if (offset < 0) {
|
|
1127
|
+
skipBytes(reader, channelSize);
|
|
1128
|
+
} else {
|
|
1129
|
+
readDataRaw(readBytes(reader, channelSize), pixelData, bitsPerChannel, step, offset);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
} else if (compression === Compression.RleCompressed) {
|
|
1133
|
+
readDataRLE(reader, pixelData, pixelData.width, pixelData.height, bitsPerChannel, step, offsets, reader.large);
|
|
1134
|
+
} else {
|
|
1135
|
+
throw new Error(`Compression not supported: ${compression}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// un-premultiplies colors from white matte, used for composite image of documents with global alpha
|
|
1140
|
+
function removeWhiteMatte({ data, width, height }: PixelData) {
|
|
1141
|
+
const size = width * height * 4;
|
|
1142
|
+
|
|
1143
|
+
if (data instanceof Float32Array) {
|
|
1144
|
+
for (let i = 0; i < size; i += 4) {
|
|
1145
|
+
const a = data[i + 3];
|
|
1146
|
+
|
|
1147
|
+
if (a > 0 && a < 1) {
|
|
1148
|
+
const ra = 1 / a;
|
|
1149
|
+
const invA = 1 - ra;
|
|
1150
|
+
|
|
1151
|
+
for (let j = 0; j < 3; j++) {
|
|
1152
|
+
const v = data[i + j] * ra + invA;
|
|
1153
|
+
data[i + j] = v < 0 ? 0 : v;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} else if (data instanceof Uint16Array) {
|
|
1158
|
+
for (let i = 0; i < size; i += 4) {
|
|
1159
|
+
const pa = data[i + 3];
|
|
1160
|
+
|
|
1161
|
+
if (pa !== 0 && pa !== 0xffff) {
|
|
1162
|
+
const ra = 0xffff / pa;
|
|
1163
|
+
const invA = 0xffff * (1 - ra);
|
|
1164
|
+
|
|
1165
|
+
for (let j = 0; j < 3; j++) {
|
|
1166
|
+
const v = Math.round(data[i + j] * ra + invA);
|
|
1167
|
+
data[i + j] = v < 0 ? 0 : (v > 0xffff ? 0xffff : v);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
// Uint8ClampedArray clamps and rounds values on assignment
|
|
1015
1173
|
for (let i = 0; i < size; i += 4) {
|
|
1016
|
-
const pa =
|
|
1174
|
+
const pa = data[i + 3];
|
|
1175
|
+
|
|
1017
1176
|
if (pa != 0 && pa != 255) {
|
|
1018
1177
|
const a = pa / 255;
|
|
1019
1178
|
const ra = 1 / a;
|
|
1020
1179
|
const invA = 255 * (1 - ra);
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1180
|
+
data[i + 0] = data[i + 0] * ra + invA;
|
|
1181
|
+
data[i + 1] = data[i + 1] * ra + invA;
|
|
1182
|
+
data[i + 2] = data[i + 2] * ra + invA;
|
|
1024
1183
|
}
|
|
1025
1184
|
}
|
|
1026
1185
|
}
|
|
1186
|
+
}
|
|
1027
1187
|
|
|
1028
|
-
|
|
1188
|
+
// converts value in 0-255 range to target range, rounding and clamping it
|
|
1189
|
+
function toRange(value: number, max: number) {
|
|
1190
|
+
value = Math.round(value * (max / 255));
|
|
1191
|
+
return value <= 0 ? 0 : (value >= max ? max : value);
|
|
1029
1192
|
}
|
|
1030
1193
|
|
|
1031
|
-
|
|
1194
|
+
// Polynomial approximation of U.S. Web Coated (SWOP) v2 CMYK to RGB conversion (default CMYK profile of Photoshop).
|
|
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)
|
|
1198
|
+
// cmyk - 8 or 16 bit data with 5 channels (cmyka), rgb - 8 or 16 bit data with 4 channels (rgba)
|
|
1199
|
+
// removeMatte - colors are blended with white (no ink), used for composite image of documents with global alpha
|
|
1200
|
+
function cmykToRgb(cmyk: PixelData, rgb: PixelData, removeMatte: boolean) {
|
|
1032
1201
|
const size = rgb.width * rgb.height * 4;
|
|
1033
1202
|
const srcData = cmyk.data;
|
|
1034
1203
|
const dstData = rgb.data;
|
|
1204
|
+
const srcMax = srcData instanceof Uint16Array ? 0xffff : 0xff;
|
|
1205
|
+
const dstMax = dstData instanceof Uint16Array ? 0xffff : 0xff;
|
|
1035
1206
|
|
|
1036
1207
|
for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1208
|
+
const a = srcData[src + 4];
|
|
1209
|
+
|
|
1210
|
+
// image data stores inverted values (0 = 100% ink)
|
|
1211
|
+
let c = 1 - srcData[src] / srcMax;
|
|
1212
|
+
let m = 1 - srcData[src + 1] / srcMax;
|
|
1213
|
+
let y = 1 - srcData[src + 2] / srcMax;
|
|
1214
|
+
let k = 1 - srcData[src + 3] / srcMax;
|
|
1215
|
+
|
|
1216
|
+
if (removeMatte && a > 0 && a < srcMax) {
|
|
1217
|
+
const ra = srcMax / a;
|
|
1218
|
+
c = Math.min(1, c * ra);
|
|
1219
|
+
m = Math.min(1, m * ra);
|
|
1220
|
+
y = Math.min(1, y * ra);
|
|
1221
|
+
k = Math.min(1, k * ra);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
dstData[dst] = toRange(255 +
|
|
1225
|
+
c * (-4.387332384609988 * c + 54.48615194189176 * m + 18.82290502165302 * y + 212.25662451639585 * k - 285.2331026137004) +
|
|
1226
|
+
m * (1.7149763477362134 * m - 5.6096736904047315 * y - 17.873870861415444 * k - 5.497006427196366) +
|
|
1227
|
+
y * (-2.5217340131683033 * y - 21.248923337353073 * k + 17.5119270841813) +
|
|
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);
|
|
1239
|
+
dstData[dst + 3] = srcMax === dstMax ? a : Math.round(a * dstMax / srcMax);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// transparencyIndex - palette index of transparent color (-1 if there's none)
|
|
1244
|
+
function indexedToRgb(indexed: PixelData, rgb: PixelData, palette: RGB[], transparencyIndex: number) {
|
|
1060
1245
|
const size = indexed.width * indexed.height;
|
|
1061
1246
|
const srcData = indexed.data;
|
|
1062
1247
|
const dstData = rgb.data;
|
|
1063
1248
|
|
|
1064
1249
|
for (let src = 0, dst = 0; src < size; src++, dst += 4) {
|
|
1065
|
-
const
|
|
1250
|
+
const index = srcData[src];
|
|
1251
|
+
const c = palette[index];
|
|
1066
1252
|
dstData[dst + 0] = c.r;
|
|
1067
1253
|
dstData[dst + 1] = c.g;
|
|
1068
1254
|
dstData[dst + 2] = c.b;
|
|
1069
|
-
dstData[dst + 3] = 255;
|
|
1255
|
+
dstData[dst + 3] = index === transparencyIndex ? 0 : 255;
|
|
1070
1256
|
}
|
|
1071
1257
|
}
|
|
1072
1258
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1259
|
+
// number of bytes used to store single channel value, 1-bit data is handled as bytes
|
|
1260
|
+
function bytesPerValue(bitDepth: number) {
|
|
1261
|
+
switch (bitDepth) {
|
|
1262
|
+
case 1:
|
|
1263
|
+
case 8: return 1;
|
|
1264
|
+
case 16: return 2;
|
|
1265
|
+
case 32: return 4;
|
|
1266
|
+
default: throw new Error(`Invalid bitDepth (${bitDepth})`);
|
|
1076
1267
|
}
|
|
1077
1268
|
}
|
|
1078
1269
|
|
|
1079
|
-
function
|
|
1080
|
-
if (
|
|
1081
|
-
|
|
1082
|
-
} else if (bitDepth === 16) {
|
|
1083
|
-
// PSD files store 16-bit channel data in big-endian byte order.
|
|
1084
|
-
// Swap each pair of bytes so that Uint16Array (native-endian) reads the correct values.
|
|
1085
|
-
for (let i = 0; i < bytes.byteLength; i += 2) {
|
|
1086
|
-
const tmp = bytes[i];
|
|
1087
|
-
bytes[i] = bytes[i + 1];
|
|
1088
|
-
bytes[i + 1] = tmp;
|
|
1089
|
-
}
|
|
1090
|
-
if (bytes.byteOffset % 2) {
|
|
1091
|
-
const result = new Uint16Array(bytes.byteLength / 2);
|
|
1092
|
-
new Uint8Array(result.buffer, result.byteOffset, result.byteLength).set(bytes);
|
|
1093
|
-
return result;
|
|
1094
|
-
} else {
|
|
1095
|
-
return new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2);
|
|
1096
|
-
}
|
|
1097
|
-
} else if (bitDepth === 32) {
|
|
1098
|
-
if (bytes.byteOffset % 4) {
|
|
1099
|
-
const result = new Float32Array(bytes.byteLength / 4);
|
|
1100
|
-
new Uint8Array(result.buffer, result.byteOffset, result.byteLength).set(bytes);
|
|
1101
|
-
return result;
|
|
1102
|
-
} else {
|
|
1103
|
-
return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4);
|
|
1104
|
-
}
|
|
1105
|
-
} else {
|
|
1106
|
-
throw new Error(`Invalid bitDepth (${bitDepth})`)
|
|
1270
|
+
function verifyArrayType(data: PixelArray, bytesPerValue: number) {
|
|
1271
|
+
if (data.BYTES_PER_ELEMENT !== bytesPerValue) {
|
|
1272
|
+
throw new Error('Invalid array types');
|
|
1107
1273
|
}
|
|
1108
1274
|
}
|
|
1109
1275
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1276
|
+
// NOTE: `buffer` may alias input file data, it must not be modified here
|
|
1277
|
+
function readDataRaw(buffer: Uint8Array, pixelData: PixelData | undefined, bitDepth: number, step: number, offset: number) {
|
|
1278
|
+
const bpv = bytesPerValue(bitDepth);
|
|
1279
|
+
|
|
1280
|
+
if (!pixelData || offset < 0 || offset >= step) return;
|
|
1281
|
+
|
|
1113
1282
|
const data = pixelData.data;
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1283
|
+
verifyArrayType(data, bpv);
|
|
1284
|
+
const size = Math.min(pixelData.width * pixelData.height, Math.floor(buffer.byteLength / bpv)) | 0;
|
|
1118
1285
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const a = buffer[i + 0];
|
|
1123
|
-
const b = buffer[i + 1];
|
|
1124
|
-
const c = buffer[i + 2];
|
|
1125
|
-
const d = buffer[i + 3];
|
|
1126
|
-
buffer[i + 0] = d;
|
|
1127
|
-
buffer[i + 1] = c;
|
|
1128
|
-
buffer[i + 2] = b;
|
|
1129
|
-
buffer[i + 3] = a;
|
|
1286
|
+
if (bpv === 1) {
|
|
1287
|
+
for (let i = 0, p = offset | 0; i < size; i++, p = (p + step) | 0) {
|
|
1288
|
+
data[p] = buffer[i];
|
|
1130
1289
|
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1290
|
+
} else if (bpv === 2) {
|
|
1291
|
+
for (let i = 0, j = 0, p = offset | 0; i < size; i++, j += 2, p = (p + step) | 0) {
|
|
1292
|
+
data[p] = (buffer[j] << 8) | buffer[j + 1];
|
|
1293
|
+
}
|
|
1294
|
+
} else {
|
|
1295
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
1134
1296
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1297
|
+
for (let i = 0, j = 0, p = offset | 0; i < size; i++, j += 4, p = (p + step) | 0) {
|
|
1298
|
+
data[p] = view.getFloat32(j, false);
|
|
1299
|
+
}
|
|
1137
1300
|
}
|
|
1138
1301
|
}
|
|
1139
1302
|
|
|
1140
|
-
function decodePredicted(data: Uint8Array
|
|
1303
|
+
function decodePredicted(data: Uint8Array, width: number, height: number) {
|
|
1141
1304
|
for (let y = 0; y < height; y++) {
|
|
1142
1305
|
const offset = y * width;
|
|
1143
1306
|
|
|
1144
1307
|
for (let x = 1, o = offset + 1; x < width; x++, o++) {
|
|
1145
|
-
data[o] =
|
|
1308
|
+
data[o] = data[o - 1] + data[o]; // Uint8Array wraps values on assignment
|
|
1146
1309
|
}
|
|
1147
1310
|
}
|
|
1148
1311
|
}
|
|
1149
1312
|
|
|
1150
|
-
|
|
1151
|
-
const decompressed = inflateSync(compressed);
|
|
1313
|
+
const zipOverflow = new Error('Decompressed data exceeds expected size');
|
|
1152
1314
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1315
|
+
// inflates data, stopping when output exceeds expected size (protects against zip bombs)
|
|
1316
|
+
function inflateLimited(compressed: Uint8Array, expectedSize: number, reader: PsdReader | undefined) {
|
|
1317
|
+
const output = new Uint8Array(expectedSize);
|
|
1318
|
+
let position = 0;
|
|
1319
|
+
let overflow = false;
|
|
1155
1320
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
} else if (bitDepth === 32) {
|
|
1163
|
-
if (prediction) decodePredicted(decompressed, width * 4, height, 0x100);
|
|
1321
|
+
const inflator = new Inflate({ chunkSize: Math.max(1024, Math.min(expectedSize, 0x10000)) });
|
|
1322
|
+
inflator.onData = (chunk: any) => {
|
|
1323
|
+
const data = chunk as Uint8Array;
|
|
1324
|
+
const count = Math.min(data.length, expectedSize - position);
|
|
1325
|
+
if (count > 0) output.set(count === data.length ? data : data.subarray(0, count), position);
|
|
1326
|
+
position += count;
|
|
1164
1327
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1328
|
+
if (count < data.length) {
|
|
1329
|
+
overflow = true;
|
|
1330
|
+
throw zipOverflow;
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
inflator.onEnd = (status: number) => {
|
|
1334
|
+
// default handler collects all the chunks into result, we only need the error status
|
|
1335
|
+
inflator.err = status;
|
|
1336
|
+
inflator.msg = (inflator as any).strm?.msg || '';
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
try {
|
|
1340
|
+
inflator.push(compressed, true);
|
|
1341
|
+
} catch (e) {
|
|
1342
|
+
if (e !== zipOverflow) throw e;
|
|
1343
|
+
}
|
|
1167
1344
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1345
|
+
if (overflow) {
|
|
1346
|
+
if (reader) warnOrThrow(reader, zipOverflow.message);
|
|
1347
|
+
} else if (inflator.err) {
|
|
1348
|
+
throw new Error(`Invalid zip data: ${inflator.msg}`);
|
|
1349
|
+
}
|
|
1170
1350
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1351
|
+
return output;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
export function readDataZip(
|
|
1355
|
+
compressed: Uint8Array, pixelData: PixelData | undefined, width: number, height: number, bitDepth: number, step: number,
|
|
1356
|
+
offset: number, prediction: boolean, reader?: PsdReader
|
|
1357
|
+
) {
|
|
1358
|
+
const bpv = bytesPerValue(bitDepth);
|
|
1359
|
+
|
|
1360
|
+
if (!pixelData || offset < 0 || offset >= step) return;
|
|
1361
|
+
|
|
1362
|
+
const data = pixelData.data;
|
|
1363
|
+
verifyArrayType(data, bpv);
|
|
1364
|
+
|
|
1365
|
+
const expectedSize = width * height * bpv;
|
|
1366
|
+
if (reader) consumeMemory(reader, expectedSize);
|
|
1367
|
+
|
|
1368
|
+
try {
|
|
1369
|
+
const decompressed = inflateLimited(compressed, expectedSize, reader);
|
|
1370
|
+
|
|
1371
|
+
if (bpv === 1) {
|
|
1372
|
+
for (let y = 0, i = 0; y < height; y++) {
|
|
1373
|
+
let prev = 0;
|
|
1374
|
+
|
|
1375
|
+
for (let x = 0, p = offset + y * width * step; x < width; x++, i++, p += step) {
|
|
1376
|
+
let v = decompressed[i];
|
|
1377
|
+
if (prediction) v = prev = (prev + v) & 0xff;
|
|
1378
|
+
data[p] = v;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
} else if (bpv === 2) {
|
|
1382
|
+
for (let y = 0, i = 0; y < height; y++) {
|
|
1383
|
+
let prev = 0;
|
|
1384
|
+
|
|
1385
|
+
for (let x = 0, p = offset + y * width * step; x < width; x++, i += 2, p += step) {
|
|
1386
|
+
let v = (decompressed[i] << 8) | decompressed[i + 1];
|
|
1387
|
+
if (prediction) v = prev = (prev + v) & 0xffff;
|
|
1388
|
+
data[p] = v;
|
|
1176
1389
|
}
|
|
1177
1390
|
}
|
|
1178
1391
|
} else {
|
|
1179
|
-
|
|
1392
|
+
const dst = new Uint32Array(data.buffer, data.byteOffset, data.length);
|
|
1393
|
+
|
|
1394
|
+
if (prediction) {
|
|
1395
|
+
// bytes are delta encoded per row and stored in planar order (all 1st bytes, all 2nd bytes, ...)
|
|
1396
|
+
decodePredicted(decompressed, width * 4, height);
|
|
1397
|
+
|
|
1398
|
+
for (let y = 0, di = offset; y < height; y++) {
|
|
1399
|
+
let a = width * 4 * y;
|
|
1400
|
+
|
|
1401
|
+
for (let x = 0; x < width; x++, a++, di += step) {
|
|
1402
|
+
const b = a + width;
|
|
1403
|
+
const c = b + width;
|
|
1404
|
+
const d = c + width;
|
|
1405
|
+
dst[di] = ((decompressed[a] << 24) | (decompressed[b] << 16) | (decompressed[c] << 8) | decompressed[d]) >>> 0;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
} else {
|
|
1409
|
+
const view = new DataView(decompressed.buffer, decompressed.byteOffset, decompressed.byteLength);
|
|
1410
|
+
|
|
1411
|
+
for (let i = 0, j = 0, di = offset, size = width * height; i < size; i++, j += 4, di += step) {
|
|
1412
|
+
dst[di] = view.getUint32(j, false);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1180
1415
|
}
|
|
1416
|
+
} finally {
|
|
1417
|
+
if (reader) recoverMemory(reader, expectedSize);
|
|
1181
1418
|
}
|
|
1182
1419
|
}
|
|
1183
1420
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
let
|
|
1421
|
+
// decodes single PackBits row, returns number of decoded values
|
|
1422
|
+
function decodeRLERow(src: Uint8Array, i: number, end: number, dst: PixelArray, p: number, step: number, count: number) {
|
|
1423
|
+
let x = 0;
|
|
1187
1424
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
lengths = new Uint32Array(offsets.length * height);
|
|
1425
|
+
while (i < end && x < count) {
|
|
1426
|
+
const header = src[i++];
|
|
1191
1427
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1428
|
+
if (header > 128) {
|
|
1429
|
+
if (i >= end) break;
|
|
1430
|
+
|
|
1431
|
+
const value = src[i++];
|
|
1432
|
+
let n = 257 - header;
|
|
1433
|
+
if (n > count - x) n = count - x;
|
|
1434
|
+
x += n;
|
|
1435
|
+
|
|
1436
|
+
for (; n > 0; n--, p += step) {
|
|
1437
|
+
dst[p] = value;
|
|
1195
1438
|
}
|
|
1439
|
+
} else if (header < 128) {
|
|
1440
|
+
let n = header + 1;
|
|
1441
|
+
if (n > count - x) n = count - x;
|
|
1442
|
+
if (n > end - i) n = end - i;
|
|
1443
|
+
x += n;
|
|
1444
|
+
|
|
1445
|
+
for (; n > 0; n--, p += step) {
|
|
1446
|
+
dst[p] = src[i++];
|
|
1447
|
+
}
|
|
1448
|
+
} else {
|
|
1449
|
+
// ignore 128
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return x;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// converts big-endian 16/32-bit values from bytes and writes them to `data` starting at `p` with `step`
|
|
1457
|
+
function scatterValues(bytes: Uint8Array, count: number, bpv: number, data: PixelArray, p: number, step: number) {
|
|
1458
|
+
if (bpv === 2) {
|
|
1459
|
+
for (let x = 0, i = 0; x < count; x++, i += 2, p += step) {
|
|
1460
|
+
data[p] = (bytes[i] << 8) | bytes[i + 1];
|
|
1196
1461
|
}
|
|
1197
1462
|
} else {
|
|
1198
|
-
|
|
1199
|
-
lengths = new Uint16Array(offsets.length * height);
|
|
1463
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1200
1464
|
|
|
1201
|
-
for (let
|
|
1202
|
-
|
|
1203
|
-
lengths[li] = readUint16(reader);
|
|
1204
|
-
}
|
|
1465
|
+
for (let x = 0, i = 0; x < count; x++, i += 4, p += step) {
|
|
1466
|
+
data[p] = view.getFloat32(i, false);
|
|
1205
1467
|
}
|
|
1206
1468
|
}
|
|
1469
|
+
}
|
|
1207
1470
|
|
|
1208
|
-
|
|
1471
|
+
// decodes RLE rows of single channel, each row starts at the beginning of the image row
|
|
1472
|
+
function decodeRLEChannel(
|
|
1473
|
+
src: Uint8Array, lengths: Uint16Array | Uint32Array, li: number, pos: number, width: number, height: number,
|
|
1474
|
+
data: PixelArray, offset: number, step: number, bpv: number, row: Uint8Array | undefined
|
|
1475
|
+
) {
|
|
1476
|
+
const srcEnd = src.length;
|
|
1477
|
+
const rowSize = width * bpv;
|
|
1209
1478
|
|
|
1210
|
-
|
|
1479
|
+
for (let y = 0; y < height; y++, li++) {
|
|
1480
|
+
const rowStart = pos < srcEnd ? pos : srcEnd;
|
|
1481
|
+
pos += lengths[li];
|
|
1482
|
+
const rowEnd = pos < srcEnd ? pos : srcEnd;
|
|
1211
1483
|
|
|
1212
|
-
|
|
1213
|
-
const offset = offsets[c] | 0;
|
|
1214
|
-
const extra = c > extraLimit || offset > extraLimit;
|
|
1484
|
+
if (rowStart >= rowEnd) continue;
|
|
1215
1485
|
|
|
1216
|
-
if (
|
|
1217
|
-
|
|
1218
|
-
skipBytes(reader, lengths[li]);
|
|
1219
|
-
}
|
|
1486
|
+
if (bpv === 1) {
|
|
1487
|
+
decodeRLERow(src, rowStart, rowEnd, data, offset + y * width * step, step, width);
|
|
1220
1488
|
} else {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
let header = buffer[i];
|
|
1227
|
-
|
|
1228
|
-
if (header > 128) {
|
|
1229
|
-
const value = buffer[++i];
|
|
1230
|
-
header = (256 - header) | 0;
|
|
1231
|
-
|
|
1232
|
-
for (let j = 0; j <= header && x < width; j = (j + 1) | 0, x = (x + 1) | 0) {
|
|
1233
|
-
data[p] = value;
|
|
1234
|
-
p = (p + step) | 0;
|
|
1235
|
-
}
|
|
1236
|
-
} else if (header < 128) {
|
|
1237
|
-
for (let j = 0; j <= header && x < width; j = (j + 1) | 0, x = (x + 1) | 0) {
|
|
1238
|
-
data[p] = buffer[++i];
|
|
1239
|
-
p = (p + step) | 0;
|
|
1240
|
-
}
|
|
1241
|
-
} else {
|
|
1242
|
-
// ignore 128
|
|
1243
|
-
}
|
|
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
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1244
1494
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1495
|
+
export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined, width: number, height: number, bitDepth: number, step: number, offsets: number[], large: boolean) {
|
|
1496
|
+
const data = pixelData && pixelData.data;
|
|
1497
|
+
const rows = offsets.length * height;
|
|
1498
|
+
const lengthSize = large ? 4 : 2;
|
|
1499
|
+
|
|
1500
|
+
consumeMemory(reader, rows * lengthSize);
|
|
1501
|
+
|
|
1502
|
+
try {
|
|
1503
|
+
if ((reader.offset + rows * lengthSize) > reader.view.byteLength) {
|
|
1504
|
+
throw new Error('RLE row lengths exceed buffer length');
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const lengths = large ? new Uint32Array(rows) : new Uint16Array(rows);
|
|
1508
|
+
let total = 0;
|
|
1509
|
+
|
|
1510
|
+
if (large) {
|
|
1511
|
+
for (let i = 0; i < rows; i++) total += (lengths[i] = readUint32(reader));
|
|
1512
|
+
} else {
|
|
1513
|
+
for (let i = 0; i < rows; i++) total += (lengths[i] = readUint16(reader));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const start = reader.offset;
|
|
1517
|
+
const srcEnd = reader.view.byteLength;
|
|
1518
|
+
|
|
1519
|
+
if (total > (srcEnd - start)) {
|
|
1520
|
+
// broken or truncated file, decode only data that is available
|
|
1521
|
+
warnOrThrow(reader, 'RLE data exceeds buffer length');
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const bpv = bytesPerValue(bitDepth);
|
|
1525
|
+
if (data) verifyArrayType(data, bpv);
|
|
1526
|
+
|
|
1527
|
+
const src = new Uint8Array(reader.view.buffer, reader.view.byteOffset, srcEnd);
|
|
1528
|
+
const row = (data && bpv > 1) ? new Uint8Array(width * bpv) : undefined;
|
|
1529
|
+
let pos = start;
|
|
1530
|
+
|
|
1531
|
+
for (let c = 0, li = 0; c < offsets.length; c++, li += height) {
|
|
1532
|
+
const offset = offsets[c] | 0;
|
|
1533
|
+
const channelStart = pos;
|
|
1534
|
+
|
|
1535
|
+
for (let y = 0; y < height; y++) pos += lengths[li + y];
|
|
1536
|
+
|
|
1537
|
+
if (data && offset >= 0 && offset < step) {
|
|
1538
|
+
decodeRLEChannel(src, lengths, li, channelStart, width, height, data, offset, step, bpv, row);
|
|
1248
1539
|
}
|
|
1249
1540
|
}
|
|
1250
|
-
}
|
|
1251
1541
|
|
|
1252
|
-
|
|
1542
|
+
reader.offset = pos;
|
|
1543
|
+
} finally {
|
|
1544
|
+
recoverMemory(reader, rows * lengthSize);
|
|
1545
|
+
}
|
|
1253
1546
|
}
|
|
1254
1547
|
|
|
1255
1548
|
export function readSection<T>(
|
|
@@ -1303,19 +1596,19 @@ export function readColor(reader: PsdReader): Color {
|
|
|
1303
1596
|
skipBytes(reader, 2);
|
|
1304
1597
|
return { h, s, b };
|
|
1305
1598
|
}
|
|
1306
|
-
case ColorSpace.CMYK: {
|
|
1307
|
-
const c = readUint16(reader) / 257;
|
|
1308
|
-
const m = readUint16(reader) / 257;
|
|
1309
|
-
const y = readUint16(reader) / 257;
|
|
1310
|
-
const k = readUint16(reader) / 257;
|
|
1599
|
+
case ColorSpace.CMYK: { // 0 = 100% ink
|
|
1600
|
+
const c = (0xffff - readUint16(reader)) / 257;
|
|
1601
|
+
const m = (0xffff - readUint16(reader)) / 257;
|
|
1602
|
+
const y = (0xffff - readUint16(reader)) / 257;
|
|
1603
|
+
const k = (0xffff - readUint16(reader)) / 257;
|
|
1311
1604
|
return { c, m, y, k };
|
|
1312
1605
|
}
|
|
1313
1606
|
case ColorSpace.Lab: {
|
|
1314
1607
|
const l = readInt16(reader) / 10000;
|
|
1315
1608
|
const ta = readInt16(reader);
|
|
1316
1609
|
const tb = readInt16(reader);
|
|
1317
|
-
const a =
|
|
1318
|
-
const b =
|
|
1610
|
+
const a = normalizeLabAB(ta, 100);
|
|
1611
|
+
const b = normalizeLabAB(tb, 100);
|
|
1319
1612
|
skipBytes(reader, 2);
|
|
1320
1613
|
return { l, a, b };
|
|
1321
1614
|
}
|
|
@@ -1329,6 +1622,24 @@ export function readColor(reader: PsdReader): Color {
|
|
|
1329
1622
|
}
|
|
1330
1623
|
}
|
|
1331
1624
|
|
|
1625
|
+
const MAX_PATTERN_SIZE = 30000;
|
|
1626
|
+
|
|
1627
|
+
// reads pattern, unsupported or invalid patterns are skipped (returns undefined) unless throwForMissingFeatures is set
|
|
1628
|
+
export function readPatternSafe(reader: PsdReader): PatternInfo | undefined {
|
|
1629
|
+
let length = reader.view.getUint32(reader.offset);
|
|
1630
|
+
while (length % 4) length++;
|
|
1631
|
+
const end = reader.offset + 4 + length;
|
|
1632
|
+
|
|
1633
|
+
try {
|
|
1634
|
+
return readPattern(reader);
|
|
1635
|
+
} catch (e) {
|
|
1636
|
+
if (reader.throwForMissingFeatures) throw e;
|
|
1637
|
+
reader.logMissingFeatures && reader.log(`Skipped pattern: ${(e as Error).message}`);
|
|
1638
|
+
reader.offset = end;
|
|
1639
|
+
return undefined;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1332
1643
|
export function readPattern(reader: PsdReader): PatternInfo {
|
|
1333
1644
|
let length = readUint32(reader);
|
|
1334
1645
|
while (length % 4) length++;
|
|
@@ -1340,8 +1651,8 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1340
1651
|
const x = readInt16(reader);
|
|
1341
1652
|
const y = readInt16(reader);
|
|
1342
1653
|
|
|
1343
|
-
// we only support RGB and
|
|
1344
|
-
if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed) {
|
|
1654
|
+
// we only support RGB, grayscale, indexed and CMYK for now
|
|
1655
|
+
if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed && colorMode !== ColorMode.CMYK) {
|
|
1345
1656
|
throw new Error(`Unsupported pattern color mode: ${colorMode}`);
|
|
1346
1657
|
}
|
|
1347
1658
|
|
|
@@ -1358,7 +1669,8 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1358
1669
|
})
|
|
1359
1670
|
}
|
|
1360
1671
|
|
|
1361
|
-
|
|
1672
|
+
// some writers follow the palette with 4 extra bytes (unknown)
|
|
1673
|
+
if (reader.view.getUint32(reader.offset) !== 3) skipBytes(reader, 4);
|
|
1362
1674
|
}
|
|
1363
1675
|
|
|
1364
1676
|
// virtual memory array list
|
|
@@ -1373,6 +1685,11 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1373
1685
|
const channelsCount = readUint32(reader);
|
|
1374
1686
|
const width = right - left;
|
|
1375
1687
|
const height = bottom - top;
|
|
1688
|
+
if (width < 0 || height < 0) throw new Error('Invalid pattern bounds');
|
|
1689
|
+
if (width > MAX_PATTERN_SIZE || height > MAX_PATTERN_SIZE) throw new Error(`Pattern too large (${width}x${height})`);
|
|
1690
|
+
if (channelsCount > 64) throw new Error(`Invalid pattern channel count: ${channelsCount}`);
|
|
1691
|
+
// channels cover whole pattern, each 2 bytes of RLE data can produce at most 128 bytes
|
|
1692
|
+
if (width * height > (Math.min(end, reader.view.byteLength) - reader.offset) * 64) throw new Error('Pattern data out of bounds');
|
|
1376
1693
|
const size = width * height * 4;
|
|
1377
1694
|
consumeMemory(reader, size);
|
|
1378
1695
|
const data = new Uint8Array(size);
|
|
@@ -1381,11 +1698,21 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1381
1698
|
data[i] = 255;
|
|
1382
1699
|
}
|
|
1383
1700
|
|
|
1384
|
-
|
|
1701
|
+
// first written arrays are color channels, next one is transparency (Photoshop stores it in the last, sheet mask, array)
|
|
1702
|
+
const colorChannels = colorMode === ColorMode.RGB ? 3 : (colorMode === ColorMode.CMYK ? 4 : 1);
|
|
1703
|
+
// CMYK channels are decoded into separate buffer (5 values per pixel, inverted, 0 = 100% ink) and converted to RGB at the end
|
|
1704
|
+
const cmyk = colorMode === ColorMode.CMYK ? new Uint8Array(width * height * 5).fill(255) : undefined;
|
|
1705
|
+
if (cmyk) consumeMemory(reader, cmyk.byteLength);
|
|
1706
|
+
const alphaTarget = cmyk ? 4 : 3;
|
|
1707
|
+
|
|
1708
|
+
for (let i = 0, ch = 0; i < (channelsCount + 2) && reader.offset + 4 <= end; i++) {
|
|
1385
1709
|
const has = readUint32(reader);
|
|
1386
1710
|
if (!has) continue;
|
|
1387
1711
|
|
|
1388
1712
|
const length = readUint32(reader);
|
|
1713
|
+
if (!length) continue;
|
|
1714
|
+
if (length < 23) throw new Error('Invalid pattern channel length');
|
|
1715
|
+
|
|
1389
1716
|
const pixelDepth = readUint32(reader);
|
|
1390
1717
|
const ctop = readUint32(reader);
|
|
1391
1718
|
const cleft = readUint32(reader);
|
|
@@ -1393,82 +1720,75 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1393
1720
|
const cright = readUint32(reader);
|
|
1394
1721
|
const pixelDepth2 = readUint16(reader);
|
|
1395
1722
|
const compressionMode = readUint8(reader); // 0 - raw, 1 - rle
|
|
1396
|
-
const
|
|
1397
|
-
const
|
|
1723
|
+
const cdata = readBytes(reader, length - 23);
|
|
1724
|
+
const depth = (pixelDepth2 === 8 || pixelDepth2 === 16 || pixelDepth2 === 32) ? pixelDepth2 : pixelDepth;
|
|
1398
1725
|
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1726
|
+
if (depth !== 8 && depth !== 16 && depth !== 32) throw new Error(`Unsupported pattern pixel depth: ${depth}`);
|
|
1727
|
+
|
|
1728
|
+
if (ctop < top || cleft < left || cbottom > bottom || cright > right || cbottom < ctop || cright < cleft) {
|
|
1729
|
+
throw new Error('Pattern channel bounds outside of pattern bounds');
|
|
1401
1730
|
}
|
|
1402
1731
|
|
|
1732
|
+
const target = ch < colorChannels ? ch : (ch === colorChannels ? alphaTarget : -1);
|
|
1733
|
+
ch++;
|
|
1734
|
+
|
|
1735
|
+
if (target === -1) continue;
|
|
1736
|
+
|
|
1403
1737
|
const w = cright - cleft;
|
|
1404
1738
|
const h = cbottom - ctop;
|
|
1405
|
-
const
|
|
1406
|
-
const
|
|
1739
|
+
const rowBytes = w * (depth / 8);
|
|
1740
|
+
const channelSize = rowBytes * h;
|
|
1741
|
+
let bytes: Uint8Array;
|
|
1407
1742
|
|
|
1408
1743
|
if (compressionMode === 0) {
|
|
1409
|
-
if (
|
|
1410
|
-
|
|
1411
|
-
for (let x = 0; x < w; x++) {
|
|
1412
|
-
const src = x + y * w;
|
|
1413
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1414
|
-
data[dst + ch] = cdata[src];
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
} else if (colorMode === ColorMode.Grayscale && ch < 1) {
|
|
1418
|
-
for (let y = 0; y < h; y++) {
|
|
1419
|
-
for (let x = 0; x < w; x++) {
|
|
1420
|
-
const src = x + y * w;
|
|
1421
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1422
|
-
const value = cdata[src];
|
|
1423
|
-
data[dst + 0] = value;
|
|
1424
|
-
data[dst + 1] = value;
|
|
1425
|
-
data[dst + 2] = value;
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
} else if (colorMode === ColorMode.Indexed) {
|
|
1429
|
-
for (let y = 0; y < h; y++) {
|
|
1430
|
-
for (let x = 0; x < w; x++) {
|
|
1431
|
-
const src = x + y * w;
|
|
1432
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1433
|
-
const index = cdata[src];
|
|
1434
|
-
const color = palette[index];
|
|
1435
|
-
data[dst + 0] = color.r;
|
|
1436
|
-
data[dst + 1] = color.g;
|
|
1437
|
-
data[dst + 2] = color.b;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
} else {
|
|
1441
|
-
if (reader.throwForMissingFeatures) throw new Error('Invalid color pattern');
|
|
1442
|
-
}
|
|
1744
|
+
if (cdata.byteLength < channelSize) throw new Error('Pattern channel data out of bounds');
|
|
1745
|
+
bytes = cdata;
|
|
1443
1746
|
} else if (compressionMode === 1) {
|
|
1444
|
-
|
|
1747
|
+
// each 2 bytes of RLE data can produce at most 128 bytes
|
|
1748
|
+
if (channelSize > cdata.byteLength * 64) throw new Error('Pattern channel data out of bounds');
|
|
1445
1749
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1750
|
+
consumeMemory(reader, channelSize);
|
|
1751
|
+
bytes = new Uint8Array(channelSize);
|
|
1448
1752
|
const cdataReader = createReader(cdata.buffer, cdata.byteOffset, cdata.byteLength);
|
|
1753
|
+
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
|
+
}
|
|
1449
1757
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
copyChannelToRGBA(tempData, pixelData, ox, oy, ch);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
if (colorMode === ColorMode.Grayscale && ch < 1) {
|
|
1456
|
-
readDataRLE(cdataReader, tempData, w, h, 8, 1, [0], false);
|
|
1457
|
-
copyChannelToRGBA(tempData, pixelData, ox, oy, 0);
|
|
1458
|
-
setupGrayscale(pixelData);
|
|
1459
|
-
}
|
|
1758
|
+
const ox = cleft - left;
|
|
1759
|
+
const oy = ctop - top;
|
|
1460
1760
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1761
|
+
for (let y = 0; y < h; y++) {
|
|
1762
|
+
for (let x = 0; x < w; x++) {
|
|
1763
|
+
const src = x + y * w;
|
|
1764
|
+
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1765
|
+
const value = depth === 8 ? bytes[src] : patternValueTo8bit(bytes, src, depth);
|
|
1766
|
+
|
|
1767
|
+
if (cmyk) {
|
|
1768
|
+
cmyk[(ox + x + (y + oy) * width) * 5 + target] = value;
|
|
1769
|
+
} else if (target === 3) {
|
|
1770
|
+
data[dst + 3] = value;
|
|
1771
|
+
} else if (colorMode === ColorMode.Indexed) {
|
|
1772
|
+
const color = palette[value];
|
|
1773
|
+
data[dst + 0] = color.r;
|
|
1774
|
+
data[dst + 1] = color.g;
|
|
1775
|
+
data[dst + 2] = color.b;
|
|
1776
|
+
} else if (colorMode === ColorMode.Grayscale) {
|
|
1777
|
+
data[dst + 0] = value;
|
|
1778
|
+
data[dst + 1] = value;
|
|
1779
|
+
data[dst + 2] = value;
|
|
1780
|
+
} else {
|
|
1781
|
+
data[dst + target] = value;
|
|
1782
|
+
}
|
|
1464
1783
|
}
|
|
1465
|
-
|
|
1466
|
-
recoverMemory(reader, w * h);
|
|
1467
|
-
} else {
|
|
1468
|
-
throw new Error('Invalid pattern compression mode');
|
|
1469
1784
|
}
|
|
1470
1785
|
|
|
1471
|
-
|
|
1786
|
+
if (compressionMode === 1) recoverMemory(reader, channelSize);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (cmyk) {
|
|
1790
|
+
cmykToRgb({ width, height, data: cmyk }, { width, height, data }, false);
|
|
1791
|
+
recoverMemory(reader, cmyk.byteLength);
|
|
1472
1792
|
}
|
|
1473
1793
|
|
|
1474
1794
|
reader.offset = end;
|
|
@@ -1476,22 +1796,20 @@ export function readPattern(reader: PsdReader): PatternInfo {
|
|
|
1476
1796
|
return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data };
|
|
1477
1797
|
}
|
|
1478
1798
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1488
|
-
const value = srcData.data[src];
|
|
1489
|
-
dstData.data[dst + offset] = value;
|
|
1490
|
-
}
|
|
1799
|
+
// converts big-endian 16bit or 32bit (float) value at given index to 8bit value
|
|
1800
|
+
function patternValueTo8bit(bytes: Uint8Array, index: number, depth: number) {
|
|
1801
|
+
if (depth === 16) {
|
|
1802
|
+
return Math.round(((bytes[index * 2] << 8) | bytes[index * 2 + 1]) / 257);
|
|
1803
|
+
} else {
|
|
1804
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + index * 4, 4);
|
|
1805
|
+
const value = view.getFloat32(0, false);
|
|
1806
|
+
return value > 0 ? (value < 1 ? Math.round(value * 255) : 255) : 0;
|
|
1491
1807
|
}
|
|
1492
1808
|
}
|
|
1493
1809
|
|
|
1494
|
-
function consumeMemory(reader: PsdReader, size: number) {
|
|
1810
|
+
export function consumeMemory(reader: PsdReader, size: number) {
|
|
1811
|
+
if (!(size >= 0)) throw new Error('Invalid size');
|
|
1812
|
+
|
|
1495
1813
|
if (reader.totalMemoryLimit !== undefined) {
|
|
1496
1814
|
if (reader.totalMemoryLimit < size) throw new Error('Exceeded memory limit');
|
|
1497
1815
|
reader.totalMemoryLimit -= size;
|