ag-psd 15.0.4 → 15.0.5
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/package.json +1 -1
- package/src/abr.ts +540 -0
- package/src/additionalInfo.ts +2688 -0
- package/src/csh.ts +44 -0
- package/src/descriptor.ts +1804 -0
- package/src/effectsHelpers.ts +305 -0
- package/src/engineData.ts +359 -0
- package/src/helpers.ts +387 -0
- package/src/imageResources.ts +1439 -0
- package/src/index.ts +44 -0
- package/src/initializeCanvas.ts +25 -0
- package/src/jpeg.ts +1166 -0
- package/src/psd.ts +1184 -0
- package/src/psdReader.ts +1091 -0
- package/src/psdWriter.ts +754 -0
- package/src/text.ts +751 -0
- package/src/utf8.ts +160 -0
package/src/psdReader.ts
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
import { inflate } from 'pako';
|
|
2
|
+
import {
|
|
3
|
+
Psd, Layer, ColorMode, SectionDividerType, LayerAdditionalInfo, ReadOptions, LayerMaskData, Color,
|
|
4
|
+
PatternInfo, GlobalLayerMaskInfo, RGB
|
|
5
|
+
} from './psd';
|
|
6
|
+
import {
|
|
7
|
+
resetImageData, offsetForChannel, decodeBitmap, PixelData, createCanvas, createImageData,
|
|
8
|
+
toBlendMode, ChannelID, Compression, LayerMaskFlags, MaskParams, ColorSpace, RAW_IMAGE_DATA, largeAdditionalInfoKeys
|
|
9
|
+
} from './helpers';
|
|
10
|
+
import { infoHandlersMap } from './additionalInfo';
|
|
11
|
+
import { resourceHandlersMap } from './imageResources';
|
|
12
|
+
|
|
13
|
+
interface ChannelInfo {
|
|
14
|
+
id: ChannelID;
|
|
15
|
+
length: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ReadOptionsExt extends ReadOptions {
|
|
19
|
+
large: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB];
|
|
23
|
+
const colorModes = ['bitmap', 'grayscale', 'indexed', 'RGB', 'CMYK', 'multichannel', 'duotone', 'lab'];
|
|
24
|
+
|
|
25
|
+
function setupGrayscale(data: PixelData) {
|
|
26
|
+
const size = data.width * data.height * 4;
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < size; i += 4) {
|
|
29
|
+
data.data[i + 1] = data.data[i];
|
|
30
|
+
data.data[i + 2] = data.data[i];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PsdReader {
|
|
35
|
+
offset: number;
|
|
36
|
+
view: DataView;
|
|
37
|
+
strict: boolean;
|
|
38
|
+
debug: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createReader(buffer: ArrayBuffer, offset?: number, length?: number): PsdReader {
|
|
42
|
+
const view = new DataView(buffer, offset, length);
|
|
43
|
+
return { view, offset: 0, strict: false, debug: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function warnOrThrow(reader: PsdReader, message: string) {
|
|
47
|
+
if (reader.strict) throw new Error(message);
|
|
48
|
+
if (reader.debug) console.warn(message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function readUint8(reader: PsdReader) {
|
|
52
|
+
reader.offset += 1;
|
|
53
|
+
return reader.view.getUint8(reader.offset - 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function peekUint8(reader: PsdReader) {
|
|
57
|
+
return reader.view.getUint8(reader.offset);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function readInt16(reader: PsdReader) {
|
|
61
|
+
reader.offset += 2;
|
|
62
|
+
return reader.view.getInt16(reader.offset - 2, false);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readUint16(reader: PsdReader) {
|
|
66
|
+
reader.offset += 2;
|
|
67
|
+
return reader.view.getUint16(reader.offset - 2, false);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readInt32(reader: PsdReader) {
|
|
71
|
+
reader.offset += 4;
|
|
72
|
+
return reader.view.getInt32(reader.offset - 4, false);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readInt32LE(reader: PsdReader) {
|
|
76
|
+
reader.offset += 4;
|
|
77
|
+
return reader.view.getInt32(reader.offset - 4, true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function readUint32(reader: PsdReader) {
|
|
81
|
+
reader.offset += 4;
|
|
82
|
+
return reader.view.getUint32(reader.offset - 4, false);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readFloat32(reader: PsdReader) {
|
|
86
|
+
reader.offset += 4;
|
|
87
|
+
return reader.view.getFloat32(reader.offset - 4, false);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readFloat64(reader: PsdReader) {
|
|
91
|
+
reader.offset += 8;
|
|
92
|
+
return reader.view.getFloat64(reader.offset - 8, false);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 32-bit fixed-point number 16.16
|
|
96
|
+
export function readFixedPoint32(reader: PsdReader): number {
|
|
97
|
+
return readInt32(reader) / (1 << 16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 32-bit fixed-point number 8.24
|
|
101
|
+
export function readFixedPointPath32(reader: PsdReader): number {
|
|
102
|
+
return readInt32(reader) / (1 << 24);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readBytes(reader: PsdReader, length: number) {
|
|
106
|
+
const start = reader.view.byteOffset + reader.offset;
|
|
107
|
+
reader.offset += length;
|
|
108
|
+
|
|
109
|
+
if ((start + length) > reader.view.buffer.byteLength) {
|
|
110
|
+
// fix for broken PSD files that are missing part of file at the end
|
|
111
|
+
warnOrThrow(reader, 'Reading bytes exceeding buffer length');
|
|
112
|
+
if (length > (100 * 1024 * 1024)) throw new Error('Reading past end of file'); // limit to 100MB
|
|
113
|
+
const result = new Uint8Array(length);
|
|
114
|
+
const len = Math.min(length, reader.view.byteLength - start);
|
|
115
|
+
if (len > 0) result.set(new Uint8Array(reader.view.buffer, start, len));
|
|
116
|
+
return result;
|
|
117
|
+
} else {
|
|
118
|
+
return new Uint8Array(reader.view.buffer, start, length);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function readSignature(reader: PsdReader) {
|
|
123
|
+
return readShortString(reader, 4);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function readPascalString(reader: PsdReader, padTo: number) {
|
|
127
|
+
let length = readUint8(reader);
|
|
128
|
+
const text = length ? readShortString(reader, length) : '';
|
|
129
|
+
|
|
130
|
+
while (++length % padTo) {
|
|
131
|
+
reader.offset++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readUnicodeString(reader: PsdReader) {
|
|
138
|
+
const length = readUint32(reader);
|
|
139
|
+
return readUnicodeStringWithLength(reader, length);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function readUnicodeStringWithLength(reader: PsdReader, length: number) {
|
|
143
|
+
let text = '';
|
|
144
|
+
|
|
145
|
+
while (length--) {
|
|
146
|
+
const value = readUint16(reader);
|
|
147
|
+
|
|
148
|
+
if (value || length > 0) { // remove trailing \0
|
|
149
|
+
text += String.fromCharCode(value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function readAsciiString(reader: PsdReader, length: number) {
|
|
157
|
+
let text = '';
|
|
158
|
+
|
|
159
|
+
while (length--) {
|
|
160
|
+
text += String.fromCharCode(readUint8(reader));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function skipBytes(reader: PsdReader, count: number) {
|
|
167
|
+
reader.offset += count;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function checkSignature(reader: PsdReader, a: string, b?: string) {
|
|
171
|
+
const offset = reader.offset;
|
|
172
|
+
const signature = readSignature(reader);
|
|
173
|
+
|
|
174
|
+
if (signature !== a && signature !== b) {
|
|
175
|
+
throw new Error(`Invalid signature: '${signature}' at 0x${offset.toString(16)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readShortString(reader: PsdReader, length: number) {
|
|
180
|
+
const buffer = readBytes(reader, length);
|
|
181
|
+
let result = '';
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
184
|
+
result += String.fromCharCode(buffer[i]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isValidSignature(sig: string) {
|
|
191
|
+
return sig === '8BIM' || sig === 'MeSa' || sig === 'AgHg' || sig === 'PHUT' || sig === 'DCSR';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function readPsd(reader: PsdReader, options: ReadOptions = {}) {
|
|
195
|
+
// header
|
|
196
|
+
checkSignature(reader, '8BPS');
|
|
197
|
+
const version = readUint16(reader);
|
|
198
|
+
if (version !== 1 && version !== 2) throw new Error(`Invalid PSD file version: ${version}`);
|
|
199
|
+
|
|
200
|
+
skipBytes(reader, 6);
|
|
201
|
+
const channels = readUint16(reader);
|
|
202
|
+
const height = readUint32(reader);
|
|
203
|
+
const width = readUint32(reader);
|
|
204
|
+
const bitsPerChannel = readUint16(reader);
|
|
205
|
+
const colorMode = readUint16(reader);
|
|
206
|
+
const maxSize = version === 1 ? 30000 : 300000;
|
|
207
|
+
|
|
208
|
+
if (width > maxSize || height > maxSize) throw new Error(`Invalid size`);
|
|
209
|
+
if (channels > 16) throw new Error(`Invalid channel count`);
|
|
210
|
+
if (bitsPerChannel > 32) throw new Error(`Invalid bitsPerChannel count`);
|
|
211
|
+
if (supportedColorModes.indexOf(colorMode) === -1)
|
|
212
|
+
throw new Error(`Color mode not supported: ${colorModes[colorMode] ?? colorMode}`);
|
|
213
|
+
|
|
214
|
+
const psd: Psd = { width, height, channels, bitsPerChannel, colorMode };
|
|
215
|
+
const opt: ReadOptionsExt = { ...options, large: version === 2 };
|
|
216
|
+
const fixOffsets = [0, 1, -1, 2, -2, 3, -3, 4, -4];
|
|
217
|
+
|
|
218
|
+
// color mode data
|
|
219
|
+
readSection(reader, 1, left => {
|
|
220
|
+
if (opt.throwForMissingFeatures) throw new Error('Color mode data not supported');
|
|
221
|
+
skipBytes(reader, left());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// image resources
|
|
225
|
+
readSection(reader, 1, left => {
|
|
226
|
+
while (left()) {
|
|
227
|
+
const sigOffset = reader.offset;
|
|
228
|
+
let sig = '';
|
|
229
|
+
|
|
230
|
+
// attempt to fix broken document by realigning with the signature
|
|
231
|
+
for (const offset of fixOffsets) {
|
|
232
|
+
try {
|
|
233
|
+
reader.offset = sigOffset + offset;
|
|
234
|
+
sig = readSignature(reader);
|
|
235
|
+
} catch { }
|
|
236
|
+
if (isValidSignature(sig)) break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isValidSignature(sig)) {
|
|
240
|
+
throw new Error(`Invalid signature: '${sig}' at 0x${(sigOffset).toString(16)}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const id = readUint16(reader);
|
|
244
|
+
readPascalString(reader, 2); // name
|
|
245
|
+
|
|
246
|
+
readSection(reader, 2, left => {
|
|
247
|
+
const handler = resourceHandlersMap[id];
|
|
248
|
+
const skip = id === 1036 && !!opt.skipThumbnail;
|
|
249
|
+
|
|
250
|
+
if (!psd.imageResources) {
|
|
251
|
+
psd.imageResources = {};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (handler && !skip) {
|
|
255
|
+
try {
|
|
256
|
+
handler.read(reader, psd.imageResources, left, opt);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (opt.throwForMissingFeatures) throw e;
|
|
259
|
+
skipBytes(reader, left());
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
// options.logMissingFeatures && console.log(`Unhandled image resource: ${id}`);
|
|
263
|
+
skipBytes(reader, left());
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// layer and mask info
|
|
270
|
+
let globalAlpha = false;
|
|
271
|
+
|
|
272
|
+
readSection(reader, 1, left => {
|
|
273
|
+
globalAlpha = readLayerInfo(reader, psd, opt);
|
|
274
|
+
|
|
275
|
+
// SAI does not include this section
|
|
276
|
+
if (left() > 0) {
|
|
277
|
+
const globalLayerMaskInfo = readGlobalLayerMaskInfo(reader);
|
|
278
|
+
if (globalLayerMaskInfo) psd.globalLayerMaskInfo = globalLayerMaskInfo;
|
|
279
|
+
} else {
|
|
280
|
+
// revert back to end of section if exceeded section limits
|
|
281
|
+
// opt.logMissingFeatures && console.log('reverting to end of section');
|
|
282
|
+
skipBytes(reader, left());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
while (left() > 0) {
|
|
286
|
+
// sometimes there are empty bytes here
|
|
287
|
+
while (left() && peekUint8(reader) === 0) {
|
|
288
|
+
// opt.logMissingFeatures && console.log('skipping 0 byte');
|
|
289
|
+
skipBytes(reader, 1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (left() >= 12) {
|
|
293
|
+
readAdditionalLayerInfo(reader, psd, psd, opt);
|
|
294
|
+
} else {
|
|
295
|
+
// opt.logMissingFeatures && console.log('skipping leftover bytes', left());
|
|
296
|
+
skipBytes(reader, left());
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}, undefined, opt.large);
|
|
300
|
+
|
|
301
|
+
const hasChildren = psd.children && psd.children.length;
|
|
302
|
+
const skipComposite = opt.skipCompositeImageData && (opt.skipLayerImageData || hasChildren);
|
|
303
|
+
|
|
304
|
+
if (!skipComposite) {
|
|
305
|
+
readImageData(reader, psd, globalAlpha, opt);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// TODO: show converted color mode instead of original PSD file color mode
|
|
309
|
+
// but add option to preserve file color mode (need to return image data instead of canvas in that case)
|
|
310
|
+
// psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB
|
|
311
|
+
|
|
312
|
+
return psd;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readLayerInfo(reader: PsdReader, psd: Psd, options: ReadOptionsExt) {
|
|
316
|
+
let globalAlpha = false;
|
|
317
|
+
|
|
318
|
+
readSection(reader, 2, left => {
|
|
319
|
+
let layerCount = readInt16(reader);
|
|
320
|
+
|
|
321
|
+
if (layerCount < 0) {
|
|
322
|
+
globalAlpha = true;
|
|
323
|
+
layerCount = -layerCount;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const layers: Layer[] = [];
|
|
327
|
+
const layerChannels: ChannelInfo[][] = [];
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < layerCount; i++) {
|
|
330
|
+
const { layer, channels } = readLayerRecord(reader, psd, options);
|
|
331
|
+
layers.push(layer);
|
|
332
|
+
layerChannels.push(channels);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!options.skipLayerImageData) {
|
|
336
|
+
for (let i = 0; i < layerCount; i++) {
|
|
337
|
+
readLayerChannelImageData(reader, psd, layers[i], layerChannels[i], options);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
skipBytes(reader, left());
|
|
342
|
+
|
|
343
|
+
if (!psd.children) psd.children = [];
|
|
344
|
+
|
|
345
|
+
const stack: (Layer | Psd)[] = [psd];
|
|
346
|
+
|
|
347
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
348
|
+
const l = layers[i];
|
|
349
|
+
const type = l.sectionDivider ? l.sectionDivider.type : SectionDividerType.Other;
|
|
350
|
+
|
|
351
|
+
if (type === SectionDividerType.OpenFolder || type === SectionDividerType.ClosedFolder) {
|
|
352
|
+
l.opened = type === SectionDividerType.OpenFolder;
|
|
353
|
+
l.children = [];
|
|
354
|
+
stack[stack.length - 1].children!.unshift(l);
|
|
355
|
+
stack.push(l);
|
|
356
|
+
} else if (type === SectionDividerType.BoundingSectionDivider) {
|
|
357
|
+
stack.pop();
|
|
358
|
+
// this was workaround because I didn't know what `lsdk` section was, now it's probably not needed anymore
|
|
359
|
+
// } else if (l.name === '</Layer group>' && !l.sectionDivider && !l.top && !l.left && !l.bottom && !l.right) {
|
|
360
|
+
// // sometimes layer group terminator doesn't have sectionDivider, so we just guess here (PS bug ?)
|
|
361
|
+
// stack.pop();
|
|
362
|
+
} else {
|
|
363
|
+
stack[stack.length - 1].children!.unshift(l);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}, undefined, options.large);
|
|
367
|
+
|
|
368
|
+
return globalAlpha;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readLayerRecord(reader: PsdReader, psd: Psd, options: ReadOptionsExt) {
|
|
372
|
+
const layer: Layer = {};
|
|
373
|
+
layer.top = readInt32(reader);
|
|
374
|
+
layer.left = readInt32(reader);
|
|
375
|
+
layer.bottom = readInt32(reader);
|
|
376
|
+
layer.right = readInt32(reader);
|
|
377
|
+
|
|
378
|
+
const channelCount = readUint16(reader);
|
|
379
|
+
const channels: ChannelInfo[] = [];
|
|
380
|
+
|
|
381
|
+
for (let i = 0; i < channelCount; i++) {
|
|
382
|
+
let channelID = readInt16(reader) as ChannelID;
|
|
383
|
+
let channelLength = readUint32(reader);
|
|
384
|
+
|
|
385
|
+
if (options.large) {
|
|
386
|
+
if (channelLength !== 0) throw new Error('Sizes larger than 4GB are not supported');
|
|
387
|
+
channelLength = readUint32(reader);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
channels.push({ id: channelID, length: channelLength });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
checkSignature(reader, '8BIM');
|
|
394
|
+
const blendMode = readSignature(reader);
|
|
395
|
+
if (!toBlendMode[blendMode]) throw new Error(`Invalid blend mode: '${blendMode}'`);
|
|
396
|
+
layer.blendMode = toBlendMode[blendMode];
|
|
397
|
+
|
|
398
|
+
layer.opacity = readUint8(reader) / 0xff;
|
|
399
|
+
layer.clipping = readUint8(reader) === 1;
|
|
400
|
+
|
|
401
|
+
const flags = readUint8(reader);
|
|
402
|
+
layer.transparencyProtected = (flags & 0x01) !== 0;
|
|
403
|
+
layer.hidden = (flags & 0x02) !== 0;
|
|
404
|
+
// 0x04 - obsolete
|
|
405
|
+
// 0x08 - 1 for Photoshop 5.0 and later, tells if bit 4 has useful information
|
|
406
|
+
// 0x10 - pixel data irrelevant to appearance of document
|
|
407
|
+
// 0x20 - ???
|
|
408
|
+
// if (flags & 0x20) (layer as any)._2 = true; // TEMP !!!!
|
|
409
|
+
|
|
410
|
+
skipBytes(reader, 1);
|
|
411
|
+
|
|
412
|
+
readSection(reader, 1, left => {
|
|
413
|
+
const mask = readLayerMaskData(reader, options);
|
|
414
|
+
if (mask) layer.mask = mask;
|
|
415
|
+
|
|
416
|
+
/*const blendingRanges =*/ readLayerBlendingRanges(reader);
|
|
417
|
+
layer.name = readPascalString(reader, 4);
|
|
418
|
+
|
|
419
|
+
while (left()) {
|
|
420
|
+
readAdditionalLayerInfo(reader, layer, psd, options);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return { layer, channels };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function readLayerMaskData(reader: PsdReader, options: ReadOptions) {
|
|
428
|
+
return readSection<LayerMaskData | undefined>(reader, 1, left => {
|
|
429
|
+
if (!left()) return undefined;
|
|
430
|
+
|
|
431
|
+
const mask: LayerMaskData = {};
|
|
432
|
+
mask.top = readInt32(reader);
|
|
433
|
+
mask.left = readInt32(reader);
|
|
434
|
+
mask.bottom = readInt32(reader);
|
|
435
|
+
mask.right = readInt32(reader);
|
|
436
|
+
mask.defaultColor = readUint8(reader);
|
|
437
|
+
|
|
438
|
+
const flags = readUint8(reader);
|
|
439
|
+
mask.positionRelativeToLayer = (flags & LayerMaskFlags.PositionRelativeToLayer) !== 0;
|
|
440
|
+
mask.disabled = (flags & LayerMaskFlags.LayerMaskDisabled) !== 0;
|
|
441
|
+
mask.fromVectorData = (flags & LayerMaskFlags.LayerMaskFromRenderingOtherData) !== 0;
|
|
442
|
+
|
|
443
|
+
if (flags & LayerMaskFlags.MaskHasParametersAppliedToIt) {
|
|
444
|
+
const params = readUint8(reader);
|
|
445
|
+
if (params & MaskParams.UserMaskDensity) mask.userMaskDensity = readUint8(reader) / 0xff;
|
|
446
|
+
if (params & MaskParams.UserMaskFeather) mask.userMaskFeather = readFloat64(reader);
|
|
447
|
+
if (params & MaskParams.VectorMaskDensity) mask.vectorMaskDensity = readUint8(reader) / 0xff;
|
|
448
|
+
if (params & MaskParams.VectorMaskFeather) mask.vectorMaskFeather = readFloat64(reader);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (left() > 2) {
|
|
452
|
+
options.logMissingFeatures && console.log('Unhandled extra mask params');
|
|
453
|
+
// TODO: handle these values
|
|
454
|
+
/*const realFlags =*/ readUint8(reader);
|
|
455
|
+
/*const realUserMaskBackground =*/ readUint8(reader);
|
|
456
|
+
/*const top2 =*/ readInt32(reader);
|
|
457
|
+
/*const left2 =*/ readInt32(reader);
|
|
458
|
+
/*const bottom2 =*/ readInt32(reader);
|
|
459
|
+
/*const right2 =*/ readInt32(reader);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
skipBytes(reader, left());
|
|
463
|
+
return mask;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function readLayerBlendingRanges(reader: PsdReader) {
|
|
468
|
+
return readSection(reader, 1, left => {
|
|
469
|
+
const compositeGrayBlendSource = readUint32(reader);
|
|
470
|
+
const compositeGraphBlendDestinationRange = readUint32(reader);
|
|
471
|
+
const ranges = [];
|
|
472
|
+
|
|
473
|
+
while (left()) {
|
|
474
|
+
const sourceRange = readUint32(reader);
|
|
475
|
+
const destRange = readUint32(reader);
|
|
476
|
+
ranges.push({ sourceRange, destRange });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { compositeGrayBlendSource, compositeGraphBlendDestinationRange, ranges };
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function readLayerChannelImageData(
|
|
484
|
+
reader: PsdReader, psd: Psd, layer: Layer, channels: ChannelInfo[], options: ReadOptionsExt
|
|
485
|
+
) {
|
|
486
|
+
const layerWidth = (layer.right || 0) - (layer.left || 0);
|
|
487
|
+
const layerHeight = (layer.bottom || 0) - (layer.top || 0);
|
|
488
|
+
const cmyk = psd.colorMode === ColorMode.CMYK;
|
|
489
|
+
|
|
490
|
+
let imageData: ImageData | undefined;
|
|
491
|
+
|
|
492
|
+
if (layerWidth && layerHeight) {
|
|
493
|
+
if (cmyk) {
|
|
494
|
+
imageData = { width: layerWidth, height: layerHeight, data: new Uint8ClampedArray(layerWidth * layerHeight * 5) } as any as ImageData;
|
|
495
|
+
for (let p = 4; p < imageData.data.byteLength; p += 5) imageData.data[p] = 255;
|
|
496
|
+
} else {
|
|
497
|
+
imageData = createImageData(layerWidth, layerHeight);
|
|
498
|
+
resetImageData(imageData);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (RAW_IMAGE_DATA) (layer as any).imageDataRaw = [];
|
|
503
|
+
|
|
504
|
+
for (const channel of channels) {
|
|
505
|
+
if (channel.length === 0) continue;
|
|
506
|
+
if (channel.length < 2) throw new Error('Invalid channel length');
|
|
507
|
+
|
|
508
|
+
const start = reader.offset;
|
|
509
|
+
|
|
510
|
+
let compression = readUint16(reader) as Compression;
|
|
511
|
+
|
|
512
|
+
// try to fix broken files where there's 1 byte shift of channel
|
|
513
|
+
if (compression > 3) {
|
|
514
|
+
reader.offset -= 1;
|
|
515
|
+
compression = readUint16(reader) as Compression;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// try to fix broken files where there's 1 byte shift of channel
|
|
519
|
+
if (compression > 3) {
|
|
520
|
+
reader.offset -= 3;
|
|
521
|
+
compression = readUint16(reader) as Compression;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (compression > 3) throw new Error(`Invalid compression: ${compression}`);
|
|
525
|
+
|
|
526
|
+
if (channel.id === ChannelID.UserMask) {
|
|
527
|
+
const mask = layer.mask;
|
|
528
|
+
|
|
529
|
+
if (!mask) throw new Error(`Missing layer mask data`);
|
|
530
|
+
|
|
531
|
+
const maskWidth = (mask.right || 0) - (mask.left || 0);
|
|
532
|
+
const maskHeight = (mask.bottom || 0) - (mask.top || 0);
|
|
533
|
+
|
|
534
|
+
if (maskWidth && maskHeight) {
|
|
535
|
+
const maskData = createImageData(maskWidth, maskHeight);
|
|
536
|
+
resetImageData(maskData);
|
|
537
|
+
|
|
538
|
+
const start = reader.offset;
|
|
539
|
+
readData(reader, channel.length, maskData, compression, maskWidth, maskHeight, 0, options.large, 4);
|
|
540
|
+
|
|
541
|
+
if (RAW_IMAGE_DATA) {
|
|
542
|
+
(layer as any).maskDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
setupGrayscale(maskData);
|
|
546
|
+
|
|
547
|
+
if (options.useImageData) {
|
|
548
|
+
mask.imageData = maskData;
|
|
549
|
+
} else {
|
|
550
|
+
mask.canvas = createCanvas(maskWidth, maskHeight);
|
|
551
|
+
mask.canvas.getContext('2d')!.putImageData(maskData, 0, 0);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
const offset = offsetForChannel(channel.id, cmyk);
|
|
556
|
+
let targetData = imageData;
|
|
557
|
+
|
|
558
|
+
if (offset < 0) {
|
|
559
|
+
targetData = undefined;
|
|
560
|
+
|
|
561
|
+
if (options.throwForMissingFeatures) {
|
|
562
|
+
throw new Error(`Channel not supported: ${channel.id}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
readData(reader, channel.length, targetData, compression, layerWidth, layerHeight, offset, options.large, cmyk ? 5 : 4);
|
|
567
|
+
|
|
568
|
+
if (RAW_IMAGE_DATA) {
|
|
569
|
+
(layer as any).imageDataRaw[channel.id] = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start + 2, channel.length - 2);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
reader.offset = start + channel.length;
|
|
573
|
+
|
|
574
|
+
if (targetData && psd.colorMode === ColorMode.Grayscale) {
|
|
575
|
+
setupGrayscale(targetData);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (imageData) {
|
|
581
|
+
if (cmyk) {
|
|
582
|
+
const cmykData = imageData;
|
|
583
|
+
imageData = createImageData(cmykData.width, cmykData.height);
|
|
584
|
+
cmykToRgb(cmykData, imageData, false);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (options.useImageData) {
|
|
588
|
+
layer.imageData = imageData;
|
|
589
|
+
} else {
|
|
590
|
+
layer.canvas = createCanvas(layerWidth, layerHeight);
|
|
591
|
+
layer.canvas.getContext('2d')!.putImageData(imageData, 0, 0);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function readData(
|
|
597
|
+
reader: PsdReader, length: number, data: ImageData | undefined, compression: Compression, width: number, height: number,
|
|
598
|
+
offset: number, large: boolean, step: number
|
|
599
|
+
) {
|
|
600
|
+
if (compression === Compression.RawData) {
|
|
601
|
+
readDataRaw(reader, data, width, height, step, offset);
|
|
602
|
+
} else if (compression === Compression.RleCompressed) {
|
|
603
|
+
readDataRLE(reader, data, width, height, step, [offset], large);
|
|
604
|
+
} else if (compression === Compression.ZipWithoutPrediction) {
|
|
605
|
+
readDataZipWithoutPrediction(reader, length, data, width, height, step, offset);
|
|
606
|
+
} else if (compression === Compression.ZipWithPrediction) {
|
|
607
|
+
throw new Error(`Compression type not supported: ${compression}`);
|
|
608
|
+
} else {
|
|
609
|
+
throw new Error(`Invalid Compression type: ${compression}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function readGlobalLayerMaskInfo(reader: PsdReader) {
|
|
614
|
+
return readSection<GlobalLayerMaskInfo | undefined>(reader, 1, left => {
|
|
615
|
+
if (!left()) return undefined;
|
|
616
|
+
|
|
617
|
+
const overlayColorSpace = readUint16(reader);
|
|
618
|
+
const colorSpace1 = readUint16(reader);
|
|
619
|
+
const colorSpace2 = readUint16(reader);
|
|
620
|
+
const colorSpace3 = readUint16(reader);
|
|
621
|
+
const colorSpace4 = readUint16(reader);
|
|
622
|
+
const opacity = readUint16(reader) / 0xff;
|
|
623
|
+
const kind = readUint8(reader);
|
|
624
|
+
skipBytes(reader, left()); // 3 bytes of padding ?
|
|
625
|
+
return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind };
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function readAdditionalLayerInfo(reader: PsdReader, target: LayerAdditionalInfo, psd: Psd, options: ReadOptionsExt) {
|
|
630
|
+
const sig = readSignature(reader);
|
|
631
|
+
if (sig !== '8BIM' && sig !== '8B64') throw new Error(`Invalid signature: '${sig}' at 0x${(reader.offset - 4).toString(16)}`);
|
|
632
|
+
const key = readSignature(reader);
|
|
633
|
+
|
|
634
|
+
// `largeAdditionalInfoKeys` fallback, because some keys don't have 8B64 signature even when they are 64bit
|
|
635
|
+
const u64 = sig === '8B64' || (options.large && largeAdditionalInfoKeys.indexOf(key) !== -1);
|
|
636
|
+
|
|
637
|
+
readSection(reader, 2, left => {
|
|
638
|
+
const handler = infoHandlersMap[key];
|
|
639
|
+
|
|
640
|
+
if (handler) {
|
|
641
|
+
try {
|
|
642
|
+
handler.read(reader, target, left, psd, options);
|
|
643
|
+
} catch (e) {
|
|
644
|
+
if (options.throwForMissingFeatures) throw e;
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
options.logMissingFeatures && console.log(`Unhandled additional info: ${key}`);
|
|
648
|
+
skipBytes(reader, left());
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (left()) {
|
|
652
|
+
options.logMissingFeatures && console.log(`Unread ${left()} bytes left for additional info: ${key}`);
|
|
653
|
+
skipBytes(reader, left());
|
|
654
|
+
}
|
|
655
|
+
}, false, u64);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function readImageData(reader: PsdReader, psd: Psd, globalAlpha: boolean, options: ReadOptionsExt) {
|
|
659
|
+
const compression = readUint16(reader) as Compression;
|
|
660
|
+
|
|
661
|
+
if (supportedColorModes.indexOf(psd.colorMode!) === -1)
|
|
662
|
+
throw new Error(`Color mode not supported: ${psd.colorMode}`);
|
|
663
|
+
|
|
664
|
+
if (compression !== Compression.RawData && compression !== Compression.RleCompressed)
|
|
665
|
+
throw new Error(`Compression type not supported: ${compression}`);
|
|
666
|
+
|
|
667
|
+
const imageData = createImageData(psd.width, psd.height);
|
|
668
|
+
resetImageData(imageData);
|
|
669
|
+
|
|
670
|
+
switch (psd.colorMode) {
|
|
671
|
+
case ColorMode.Bitmap: {
|
|
672
|
+
let bytes: Uint8Array;
|
|
673
|
+
|
|
674
|
+
if (compression === Compression.RawData) {
|
|
675
|
+
bytes = readBytes(reader, Math.ceil(psd.width / 8) * psd.height);
|
|
676
|
+
} else if (compression === Compression.RleCompressed) {
|
|
677
|
+
bytes = new Uint8Array(psd.width * psd.height);
|
|
678
|
+
readDataRLE(reader, { data: bytes, width: psd.width, height: psd.height }, psd.width, psd.height, 1, [0], options.large);
|
|
679
|
+
} else {
|
|
680
|
+
throw new Error(`Bitmap compression not supported: ${compression}`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
decodeBitmap(bytes, imageData.data, psd.width, psd.height);
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case ColorMode.RGB:
|
|
687
|
+
case ColorMode.Grayscale: {
|
|
688
|
+
const channels = psd.colorMode === ColorMode.Grayscale ? [0] : [0, 1, 2];
|
|
689
|
+
|
|
690
|
+
if (psd.channels && psd.channels > 3) {
|
|
691
|
+
for (let i = 3; i < psd.channels; i++) {
|
|
692
|
+
// TODO: store these channels in additional image data
|
|
693
|
+
channels.push(i);
|
|
694
|
+
}
|
|
695
|
+
} else if (globalAlpha) {
|
|
696
|
+
channels.push(3);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (compression === Compression.RawData) {
|
|
700
|
+
for (let i = 0; i < channels.length; i++) {
|
|
701
|
+
readDataRaw(reader, imageData, psd.width, psd.height, 4, channels[i]);
|
|
702
|
+
}
|
|
703
|
+
} else if (compression === Compression.RleCompressed) {
|
|
704
|
+
const start = reader.offset;
|
|
705
|
+
readDataRLE(reader, imageData, psd.width, psd.height, 4, channels, options.large);
|
|
706
|
+
if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (psd.colorMode === ColorMode.Grayscale) {
|
|
710
|
+
setupGrayscale(imageData);
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case ColorMode.CMYK: {
|
|
715
|
+
if (psd.channels !== 4) throw new Error(`Invalid channel count`);
|
|
716
|
+
|
|
717
|
+
const channels = [0, 1, 2, 3];
|
|
718
|
+
if (globalAlpha) channels.push(4);
|
|
719
|
+
|
|
720
|
+
if (compression === Compression.RawData) {
|
|
721
|
+
throw new Error(`Not implemented`);
|
|
722
|
+
// TODO: ...
|
|
723
|
+
// for (let i = 0; i < channels.length; i++) {
|
|
724
|
+
// readDataRaw(reader, imageData, channels[i], psd.width, psd.height);
|
|
725
|
+
// }
|
|
726
|
+
} else if (compression === Compression.RleCompressed) {
|
|
727
|
+
const cmykImageData: PixelData = {
|
|
728
|
+
width: imageData.width,
|
|
729
|
+
height: imageData.height,
|
|
730
|
+
data: new Uint8Array(imageData.width * imageData.height * 5),
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const start = reader.offset;
|
|
734
|
+
readDataRLE(reader, cmykImageData, psd.width, psd.height, 5, channels, options.large);
|
|
735
|
+
cmykToRgb(cmykImageData, imageData, true);
|
|
736
|
+
|
|
737
|
+
if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
default: throw new Error(`Color mode not supported: ${psd.colorMode}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// remove weird white matte
|
|
746
|
+
if (globalAlpha) {
|
|
747
|
+
const p = imageData.data;
|
|
748
|
+
const size = imageData.width * imageData.height * 4;
|
|
749
|
+
for (let i = 0; i < size; i += 4) {
|
|
750
|
+
const pa = p[i + 3];
|
|
751
|
+
if (pa != 0 && pa != 255) {
|
|
752
|
+
const a = pa / 255;
|
|
753
|
+
const ra = 1 / a;
|
|
754
|
+
const invA = 255 * (1 - ra);
|
|
755
|
+
p[i + 0] = p[i + 0] * ra + invA;
|
|
756
|
+
p[i + 1] = p[i + 1] * ra + invA;
|
|
757
|
+
p[i + 2] = p[i + 2] * ra + invA;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (options.useImageData) {
|
|
763
|
+
psd.imageData = imageData;
|
|
764
|
+
} else {
|
|
765
|
+
psd.canvas = createCanvas(psd.width, psd.height);
|
|
766
|
+
psd.canvas.getContext('2d')!.putImageData(imageData, 0, 0);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function cmykToRgb(cmyk: PixelData, rgb: PixelData, reverseAlpha: boolean) {
|
|
771
|
+
const size = rgb.width * rgb.height * 4;
|
|
772
|
+
const srcData = cmyk.data;
|
|
773
|
+
const dstData = rgb.data;
|
|
774
|
+
|
|
775
|
+
for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
|
|
776
|
+
const c = srcData[src];
|
|
777
|
+
const m = srcData[src + 1];
|
|
778
|
+
const y = srcData[src + 2];
|
|
779
|
+
const k = srcData[src + 3];
|
|
780
|
+
dstData[dst] = ((((c * k) | 0) / 255) | 0);
|
|
781
|
+
dstData[dst + 1] = ((((m * k) | 0) / 255) | 0);
|
|
782
|
+
dstData[dst + 2] = ((((y * k) | 0) / 255) | 0);
|
|
783
|
+
dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) {
|
|
787
|
+
// const c = 1 - (srcData[src + 0] / 255);
|
|
788
|
+
// const m = 1 - (srcData[src + 1] / 255);
|
|
789
|
+
// const y = 1 - (srcData[src + 2] / 255);
|
|
790
|
+
// // const k = srcData[src + 3] / 255;
|
|
791
|
+
// dstData[dst + 0] = ((1 - c * 0.8) * 255) | 0;
|
|
792
|
+
// dstData[dst + 1] = ((1 - m * 0.8) * 255) | 0;
|
|
793
|
+
// dstData[dst + 2] = ((1 - y * 0.8) * 255) | 0;
|
|
794
|
+
// dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4];
|
|
795
|
+
// }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function readDataRaw(reader: PsdReader, pixelData: PixelData | undefined, width: number, height: number, step: number, offset: number) {
|
|
799
|
+
const size = width * height;
|
|
800
|
+
const buffer = readBytes(reader, size);
|
|
801
|
+
|
|
802
|
+
if (pixelData && offset < step) {
|
|
803
|
+
const data = pixelData.data;
|
|
804
|
+
|
|
805
|
+
for (let i = 0, p = offset | 0; i < size; i++, p = (p + step) | 0) {
|
|
806
|
+
data[p] = buffer[i];
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function readDataZipWithoutPrediction(
|
|
812
|
+
reader: PsdReader, length: number, pixelData: PixelData | undefined, width: number, height: number,
|
|
813
|
+
step: number, offset: number
|
|
814
|
+
) {
|
|
815
|
+
const compressed = readBytes(reader, length);
|
|
816
|
+
const decompressed = inflate(compressed);
|
|
817
|
+
const size = width * height;
|
|
818
|
+
|
|
819
|
+
if (pixelData && offset < step) {
|
|
820
|
+
const data = pixelData.data;
|
|
821
|
+
|
|
822
|
+
for (let i = 0, p = offset | 0; i < size; i++, p = (p + step) | 0) {
|
|
823
|
+
data[p] = decompressed[i];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export function readDataRLE(
|
|
829
|
+
reader: PsdReader, pixelData: PixelData | undefined, _width: number, height: number, step: number, offsets: number[],
|
|
830
|
+
large: boolean
|
|
831
|
+
) {
|
|
832
|
+
const data = pixelData && pixelData.data;
|
|
833
|
+
let lengths: Uint16Array | Uint32Array;
|
|
834
|
+
|
|
835
|
+
if (large) {
|
|
836
|
+
lengths = new Uint32Array(offsets.length * height);
|
|
837
|
+
|
|
838
|
+
for (let o = 0, li = 0; o < offsets.length; o++) {
|
|
839
|
+
for (let y = 0; y < height; y++, li++) {
|
|
840
|
+
lengths[li] = readUint32(reader);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
lengths = new Uint16Array(offsets.length * height);
|
|
845
|
+
|
|
846
|
+
for (let o = 0, li = 0; o < offsets.length; o++) {
|
|
847
|
+
for (let y = 0; y < height; y++, li++) {
|
|
848
|
+
lengths[li] = readUint16(reader);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const extraLimit = (step - 1) | 0; // 3 for rgb, 4 for cmyk
|
|
854
|
+
|
|
855
|
+
for (let c = 0, li = 0; c < offsets.length; c++) {
|
|
856
|
+
const offset = offsets[c] | 0;
|
|
857
|
+
const extra = c > extraLimit || offset > extraLimit;
|
|
858
|
+
|
|
859
|
+
if (!data || extra) {
|
|
860
|
+
for (let y = 0; y < height; y++, li++) {
|
|
861
|
+
skipBytes(reader, lengths[li]);
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
for (let y = 0, p = offset | 0; y < height; y++, li++) {
|
|
865
|
+
const length = lengths[li];
|
|
866
|
+
const buffer = readBytes(reader, length);
|
|
867
|
+
|
|
868
|
+
for (let i = 0; i < length; i++) {
|
|
869
|
+
let header = buffer[i];
|
|
870
|
+
|
|
871
|
+
if (header > 128) {
|
|
872
|
+
const value = buffer[++i];
|
|
873
|
+
header = (256 - header) | 0;
|
|
874
|
+
|
|
875
|
+
for (let j = 0; j <= header; j = (j + 1) | 0) {
|
|
876
|
+
data[p] = value;
|
|
877
|
+
p = (p + step) | 0;
|
|
878
|
+
}
|
|
879
|
+
} else if (header < 128) {
|
|
880
|
+
for (let j = 0; j <= header; j = (j + 1) | 0) {
|
|
881
|
+
data[p] = buffer[++i];
|
|
882
|
+
p = (p + step) | 0;
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
// ignore 128
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// This showed up on some images from non-photoshop programs, ignoring it seems to work just fine.
|
|
889
|
+
// if (i >= length) throw new Error(`Invalid RLE data: exceeded buffer size ${i}/${length}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export function readSection<T>(
|
|
897
|
+
reader: PsdReader, round: number, func: (left: () => number) => T, skipEmpty = true, eightBytes = false
|
|
898
|
+
): T | undefined {
|
|
899
|
+
let length = readUint32(reader);
|
|
900
|
+
|
|
901
|
+
if (eightBytes) {
|
|
902
|
+
if (length !== 0) throw new Error('Sizes larger than 4GB are not supported');
|
|
903
|
+
length = readUint32(reader);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (length <= 0 && skipEmpty) return undefined;
|
|
907
|
+
|
|
908
|
+
let end = reader.offset + length;
|
|
909
|
+
if (end > reader.view.byteLength) throw new Error('Section exceeds file size');
|
|
910
|
+
|
|
911
|
+
const result = func(() => end - reader.offset);
|
|
912
|
+
|
|
913
|
+
if (reader.offset !== end) {
|
|
914
|
+
if (reader.offset > end) {
|
|
915
|
+
warnOrThrow(reader, 'Exceeded section limits');
|
|
916
|
+
} else {
|
|
917
|
+
warnOrThrow(reader, `Unread section data`); // : ${end - reader.offset} bytes at 0x${reader.offset.toString(16)}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
while (end % round) end++;
|
|
922
|
+
reader.offset = end;
|
|
923
|
+
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function readColor(reader: PsdReader): Color {
|
|
928
|
+
const colorSpace = readUint16(reader) as ColorSpace;
|
|
929
|
+
|
|
930
|
+
switch (colorSpace) {
|
|
931
|
+
case ColorSpace.RGB: {
|
|
932
|
+
const r = readUint16(reader) / 257;
|
|
933
|
+
const g = readUint16(reader) / 257;
|
|
934
|
+
const b = readUint16(reader) / 257;
|
|
935
|
+
skipBytes(reader, 2);
|
|
936
|
+
return { r, g, b };
|
|
937
|
+
}
|
|
938
|
+
case ColorSpace.HSB: {
|
|
939
|
+
const h = readUint16(reader) / 0xffff;
|
|
940
|
+
const s = readUint16(reader) / 0xffff;
|
|
941
|
+
const b = readUint16(reader) / 0xffff;
|
|
942
|
+
skipBytes(reader, 2);
|
|
943
|
+
return { h, s, b };
|
|
944
|
+
}
|
|
945
|
+
case ColorSpace.CMYK: {
|
|
946
|
+
const c = readUint16(reader) / 257;
|
|
947
|
+
const m = readUint16(reader) / 257;
|
|
948
|
+
const y = readUint16(reader) / 257;
|
|
949
|
+
const k = readUint16(reader) / 257;
|
|
950
|
+
return { c, m, y, k };
|
|
951
|
+
}
|
|
952
|
+
case ColorSpace.Lab: {
|
|
953
|
+
const l = readInt16(reader) / 10000;
|
|
954
|
+
const ta = readInt16(reader);
|
|
955
|
+
const tb = readInt16(reader);
|
|
956
|
+
const a = ta < 0 ? (ta / 12800) : (ta / 12700);
|
|
957
|
+
const b = tb < 0 ? (tb / 12800) : (tb / 12700);
|
|
958
|
+
skipBytes(reader, 2);
|
|
959
|
+
return { l, a, b };
|
|
960
|
+
}
|
|
961
|
+
case ColorSpace.Grayscale: {
|
|
962
|
+
const k = readUint16(reader) * 255 / 10000;
|
|
963
|
+
skipBytes(reader, 6);
|
|
964
|
+
return { k };
|
|
965
|
+
}
|
|
966
|
+
default:
|
|
967
|
+
throw new Error('Invalid color space');
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function readPattern(reader: PsdReader): PatternInfo {
|
|
972
|
+
readUint32(reader); // length
|
|
973
|
+
const version = readUint32(reader);
|
|
974
|
+
if (version !== 1) throw new Error(`Invalid pattern version: ${version}`);
|
|
975
|
+
|
|
976
|
+
const colorMode = readUint32(reader) as ColorMode;
|
|
977
|
+
const x = readInt16(reader);
|
|
978
|
+
const y = readInt16(reader);
|
|
979
|
+
|
|
980
|
+
// we only support RGB and grayscale for now
|
|
981
|
+
if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed) {
|
|
982
|
+
throw new Error(`Unsupported pattern color mode: ${colorMode}`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
let name = readUnicodeString(reader);
|
|
986
|
+
const id = readPascalString(reader, 1);
|
|
987
|
+
const palette: RGB[] = [];
|
|
988
|
+
|
|
989
|
+
if (colorMode === ColorMode.Indexed) {
|
|
990
|
+
for (let i = 0; i < 256; i++) {
|
|
991
|
+
palette.push({
|
|
992
|
+
r: readUint8(reader),
|
|
993
|
+
g: readUint8(reader),
|
|
994
|
+
b: readUint8(reader),
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
skipBytes(reader, 4); // no idea what this is
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// virtual memory array list
|
|
1002
|
+
const version2 = readUint32(reader);
|
|
1003
|
+
if (version2 !== 3) throw new Error(`Invalid pattern VMAL version: ${version2}`);
|
|
1004
|
+
|
|
1005
|
+
readUint32(reader); // length
|
|
1006
|
+
const top = readUint32(reader);
|
|
1007
|
+
const left = readUint32(reader);
|
|
1008
|
+
const bottom = readUint32(reader);
|
|
1009
|
+
const right = readUint32(reader);
|
|
1010
|
+
const channelsCount = readUint32(reader);
|
|
1011
|
+
const width = right - left;
|
|
1012
|
+
const height = bottom - top;
|
|
1013
|
+
const data = new Uint8Array(width * height * 4);
|
|
1014
|
+
|
|
1015
|
+
for (let i = 3; i < data.byteLength; i += 4) {
|
|
1016
|
+
data[i] = 255;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
for (let i = 0, ch = 0; i < (channelsCount + 2); i++) {
|
|
1020
|
+
const has = readUint32(reader);
|
|
1021
|
+
if (!has) continue;
|
|
1022
|
+
|
|
1023
|
+
const length = readUint32(reader);
|
|
1024
|
+
const pixelDepth = readUint32(reader);
|
|
1025
|
+
const ctop = readUint32(reader);
|
|
1026
|
+
const cleft = readUint32(reader);
|
|
1027
|
+
const cbottom = readUint32(reader);
|
|
1028
|
+
const cright = readUint32(reader);
|
|
1029
|
+
const pixelDepth2 = readUint16(reader);
|
|
1030
|
+
const compressionMode = readUint8(reader); // 0 - raw, 1 - zip
|
|
1031
|
+
const dataLength = length - (4 + 16 + 2 + 1);
|
|
1032
|
+
const cdata = readBytes(reader, dataLength);
|
|
1033
|
+
|
|
1034
|
+
if (pixelDepth !== 8 || pixelDepth2 !== 8) {
|
|
1035
|
+
throw new Error('16bit pixel depth not supported for patterns');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const w = cright - cleft;
|
|
1039
|
+
const h = cbottom - ctop;
|
|
1040
|
+
const ox = cleft - left;
|
|
1041
|
+
const oy = ctop - top;
|
|
1042
|
+
|
|
1043
|
+
if (compressionMode === 0) {
|
|
1044
|
+
if (colorMode === ColorMode.RGB && ch < 3) {
|
|
1045
|
+
for (let y = 0; y < h; y++) {
|
|
1046
|
+
for (let x = 0; x < w; x++) {
|
|
1047
|
+
const src = x + y * w;
|
|
1048
|
+
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1049
|
+
data[dst + ch] = cdata[src];
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (colorMode === ColorMode.Grayscale && ch < 1) {
|
|
1055
|
+
for (let y = 0; y < h; y++) {
|
|
1056
|
+
for (let x = 0; x < w; x++) {
|
|
1057
|
+
const src = x + y * w;
|
|
1058
|
+
const dst = (ox + x + (y + oy) * width) * 4;
|
|
1059
|
+
const value = cdata[src];
|
|
1060
|
+
data[dst + 0] = value;
|
|
1061
|
+
data[dst + 1] = value;
|
|
1062
|
+
data[dst + 2] = value;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (colorMode === ColorMode.Indexed) {
|
|
1068
|
+
// TODO:
|
|
1069
|
+
throw new Error('Indexed pattern color mode not implemented');
|
|
1070
|
+
}
|
|
1071
|
+
} else if (compressionMode === 1) {
|
|
1072
|
+
// console.log({ colorMode });
|
|
1073
|
+
// require('fs').writeFileSync('zip.bin', Buffer.from(cdata));
|
|
1074
|
+
// const data = require('zlib').inflateRawSync(cdata);
|
|
1075
|
+
// const data = require('zlib').unzipSync(cdata);
|
|
1076
|
+
// console.log(data);
|
|
1077
|
+
// throw new Error('Zip compression not supported for pattern');
|
|
1078
|
+
// throw new Error('Unsupported pattern compression');
|
|
1079
|
+
console.error('Unsupported pattern compression');
|
|
1080
|
+
name += ' (failed to decode)';
|
|
1081
|
+
} else {
|
|
1082
|
+
throw new Error('Invalid pattern compression mode');
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
ch++;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// TODO: use canvas instead of data ?
|
|
1089
|
+
|
|
1090
|
+
return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data };
|
|
1091
|
+
}
|