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/psdWriter.ts
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import { Psd, Layer, LayerAdditionalInfo, ColorMode, SectionDividerType, WriteOptions, Color, GlobalLayerMaskInfo } from './psd';
|
|
2
|
+
import {
|
|
3
|
+
hasAlpha, createCanvas, writeDataRLE, PixelData, LayerChannelData, ChannelData,
|
|
4
|
+
offsetForChannel, createImageData, fromBlendMode, ChannelID, Compression, clamp,
|
|
5
|
+
LayerMaskFlags, MaskParams, ColorSpace, Bounds, largeAdditionalInfoKeys, RAW_IMAGE_DATA, writeDataZipWithoutPrediction
|
|
6
|
+
} from './helpers';
|
|
7
|
+
import { ExtendedWriteOptions, hasMultiEffects, infoHandlers } from './additionalInfo';
|
|
8
|
+
import { resourceHandlers } from './imageResources';
|
|
9
|
+
|
|
10
|
+
export interface PsdWriter {
|
|
11
|
+
offset: number;
|
|
12
|
+
buffer: ArrayBuffer;
|
|
13
|
+
view: DataView;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createWriter(size = 4096): PsdWriter {
|
|
17
|
+
const buffer = new ArrayBuffer(size);
|
|
18
|
+
const view = new DataView(buffer);
|
|
19
|
+
const offset = 0;
|
|
20
|
+
return { buffer, view, offset };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getWriterBuffer(writer: PsdWriter) {
|
|
24
|
+
return writer.buffer.slice(0, writer.offset);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getWriterBufferNoCopy(writer: PsdWriter) {
|
|
28
|
+
return new Uint8Array(writer.buffer, 0, writer.offset);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function writeUint8(writer: PsdWriter, value: number) {
|
|
32
|
+
const offset = addSize(writer, 1);
|
|
33
|
+
writer.view.setUint8(offset, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeInt16(writer: PsdWriter, value: number) {
|
|
37
|
+
const offset = addSize(writer, 2);
|
|
38
|
+
writer.view.setInt16(offset, value, false);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeUint16(writer: PsdWriter, value: number) {
|
|
42
|
+
const offset = addSize(writer, 2);
|
|
43
|
+
writer.view.setUint16(offset, value, false);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeInt32(writer: PsdWriter, value: number) {
|
|
47
|
+
const offset = addSize(writer, 4);
|
|
48
|
+
writer.view.setInt32(offset, value, false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeUint32(writer: PsdWriter, value: number) {
|
|
52
|
+
const offset = addSize(writer, 4);
|
|
53
|
+
writer.view.setUint32(offset, value, false);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeFloat32(writer: PsdWriter, value: number) {
|
|
57
|
+
const offset = addSize(writer, 4);
|
|
58
|
+
writer.view.setFloat32(offset, value, false);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeFloat64(writer: PsdWriter, value: number) {
|
|
62
|
+
const offset = addSize(writer, 8);
|
|
63
|
+
writer.view.setFloat64(offset, value, false);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 32-bit fixed-point number 16.16
|
|
67
|
+
export function writeFixedPoint32(writer: PsdWriter, value: number) {
|
|
68
|
+
writeInt32(writer, value * (1 << 16));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 32-bit fixed-point number 8.24
|
|
72
|
+
export function writeFixedPointPath32(writer: PsdWriter, value: number) {
|
|
73
|
+
writeInt32(writer, value * (1 << 24));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function writeBytes(writer: PsdWriter, buffer: Uint8Array | undefined) {
|
|
77
|
+
if (buffer) {
|
|
78
|
+
ensureSize(writer, writer.offset + buffer.length);
|
|
79
|
+
const bytes = new Uint8Array(writer.buffer);
|
|
80
|
+
bytes.set(buffer, writer.offset);
|
|
81
|
+
writer.offset += buffer.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function writeZeros(writer: PsdWriter, count: number) {
|
|
86
|
+
for (let i = 0; i < count; i++) {
|
|
87
|
+
writeUint8(writer, 0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function writeSignature(writer: PsdWriter, signature: string) {
|
|
92
|
+
if (signature.length !== 4) throw new Error(`Invalid signature: '${signature}'`);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < 4; i++) {
|
|
95
|
+
writeUint8(writer, signature.charCodeAt(i));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function writePascalString(writer: PsdWriter, text: string, padTo: number) {
|
|
100
|
+
let length = text.length;
|
|
101
|
+
writeUint8(writer, length);
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < length; i++) {
|
|
104
|
+
const code = text.charCodeAt(i);
|
|
105
|
+
writeUint8(writer, code < 128 ? code : '?'.charCodeAt(0));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
while (++length % padTo) {
|
|
109
|
+
writeUint8(writer, 0);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function writeUnicodeString(writer: PsdWriter, text: string) {
|
|
114
|
+
writeUint32(writer, text.length);
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < text.length; i++) {
|
|
117
|
+
writeUint16(writer, text.charCodeAt(i));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function writeUnicodeStringWithPadding(writer: PsdWriter, text: string) {
|
|
122
|
+
writeUint32(writer, text.length + 1);
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < text.length; i++) {
|
|
125
|
+
writeUint16(writer, text.charCodeAt(i));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
writeUint16(writer, 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getLargestLayerSize(layers: Layer[] = []): number {
|
|
132
|
+
let max = 0;
|
|
133
|
+
|
|
134
|
+
for (const layer of layers) {
|
|
135
|
+
if (layer.canvas || layer.imageData) {
|
|
136
|
+
const { width, height } = getLayerDimentions(layer);
|
|
137
|
+
max = Math.max(max, 2 * height + 2 * width * height);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (layer.children) {
|
|
141
|
+
max = Math.max(max, getLargestLayerSize(layer.children));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return max;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function writeSection(writer: PsdWriter, round: number, func: () => void, writeTotalLength = false, large = false) {
|
|
149
|
+
if (large) writeUint32(writer, 0);
|
|
150
|
+
const offset = writer.offset;
|
|
151
|
+
writeUint32(writer, 0);
|
|
152
|
+
|
|
153
|
+
func();
|
|
154
|
+
|
|
155
|
+
let length = writer.offset - offset - 4;
|
|
156
|
+
let len = length;
|
|
157
|
+
|
|
158
|
+
while ((len % round) !== 0) {
|
|
159
|
+
writeUint8(writer, 0);
|
|
160
|
+
len++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (writeTotalLength) {
|
|
164
|
+
length = len;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
writer.view.setUint32(offset, length, false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}) {
|
|
171
|
+
if (!(+psd.width > 0 && +psd.height > 0))
|
|
172
|
+
throw new Error('Invalid document size');
|
|
173
|
+
|
|
174
|
+
if ((psd.width > 30000 || psd.height > 30000) && !options.psb)
|
|
175
|
+
throw new Error('Document size is too large (max is 30000x30000, use PSB format instead)');
|
|
176
|
+
|
|
177
|
+
let imageResources = psd.imageResources || {};
|
|
178
|
+
|
|
179
|
+
const opt: ExtendedWriteOptions = { ...options, layerIds: new Set(), layerToId: new Map() };
|
|
180
|
+
|
|
181
|
+
if (opt.generateThumbnail) {
|
|
182
|
+
imageResources = { ...imageResources, thumbnail: createThumbnail(psd) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let imageData = psd.imageData;
|
|
186
|
+
|
|
187
|
+
if (!imageData && psd.canvas) {
|
|
188
|
+
imageData = psd.canvas.getContext('2d')!.getImageData(0, 0, psd.canvas.width, psd.canvas.height);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (imageData && (psd.width !== imageData.width || psd.height !== imageData.height))
|
|
192
|
+
throw new Error('Document canvas must have the same size as document');
|
|
193
|
+
|
|
194
|
+
const globalAlpha = !!imageData && hasAlpha(imageData);
|
|
195
|
+
const maxBufferSize = Math.max(getLargestLayerSize(psd.children), 4 * 2 * psd.width * psd.height + 2 * psd.height);
|
|
196
|
+
const tempBuffer = new Uint8Array(maxBufferSize);
|
|
197
|
+
|
|
198
|
+
// header
|
|
199
|
+
writeSignature(writer, '8BPS');
|
|
200
|
+
writeUint16(writer, options.psb ? 2 : 1); // version
|
|
201
|
+
writeZeros(writer, 6);
|
|
202
|
+
writeUint16(writer, globalAlpha ? 4 : 3); // channels
|
|
203
|
+
writeUint32(writer, psd.height);
|
|
204
|
+
writeUint32(writer, psd.width);
|
|
205
|
+
writeUint16(writer, 8); // bits per channel
|
|
206
|
+
writeUint16(writer, ColorMode.RGB); // we only support saving RGB right now
|
|
207
|
+
|
|
208
|
+
// color mode data
|
|
209
|
+
writeSection(writer, 1, () => {
|
|
210
|
+
// TODO: implement
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// image resources
|
|
214
|
+
writeSection(writer, 1, () => {
|
|
215
|
+
for (const handler of resourceHandlers) {
|
|
216
|
+
const has = handler.has(imageResources);
|
|
217
|
+
const count = has === false ? 0 : (has === true ? 1 : has);
|
|
218
|
+
for (let i = 0; i < count; i++) {
|
|
219
|
+
writeSignature(writer, '8BIM');
|
|
220
|
+
writeUint16(writer, handler.key);
|
|
221
|
+
writePascalString(writer, '', 2);
|
|
222
|
+
writeSection(writer, 2, () => handler.write(writer, imageResources, i));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// layer and mask info
|
|
228
|
+
writeSection(writer, 2, () => {
|
|
229
|
+
writeLayerInfo(tempBuffer, writer, psd, globalAlpha, opt);
|
|
230
|
+
writeGlobalLayerMaskInfo(writer, psd.globalLayerMaskInfo);
|
|
231
|
+
writeAdditionalLayerInfo(writer, psd, psd, opt);
|
|
232
|
+
}, undefined, !!opt.psb);
|
|
233
|
+
|
|
234
|
+
// image data
|
|
235
|
+
const channels = globalAlpha ? [0, 1, 2, 3] : [0, 1, 2];
|
|
236
|
+
const width = imageData ? imageData.width : psd.width;
|
|
237
|
+
const height = imageData ? imageData.height : psd.height;
|
|
238
|
+
const data: PixelData = { data: new Uint8Array(width * height * 4), width, height };
|
|
239
|
+
|
|
240
|
+
writeUint16(writer, Compression.RleCompressed); // Photoshop doesn't support zip compression of composite image data
|
|
241
|
+
|
|
242
|
+
if (RAW_IMAGE_DATA && (psd as any).imageDataRaw) {
|
|
243
|
+
console.log('writing raw image data');
|
|
244
|
+
writeBytes(writer, (psd as any).imageDataRaw);
|
|
245
|
+
} else {
|
|
246
|
+
if (imageData) data.data.set(new Uint8Array(imageData.data.buffer, imageData.data.byteOffset, imageData.data.byteLength));
|
|
247
|
+
|
|
248
|
+
// add weird white matte
|
|
249
|
+
if (globalAlpha) {
|
|
250
|
+
const size = data.width * data.height * 4;
|
|
251
|
+
const p = data.data;
|
|
252
|
+
for (let i = 0; i < size; i += 4) {
|
|
253
|
+
const pa = p[i + 3];
|
|
254
|
+
if (pa != 0 && pa != 255) {
|
|
255
|
+
const a = pa / 255;
|
|
256
|
+
const ra = 255 * (1 - a);
|
|
257
|
+
p[i + 0] = p[i + 0] * a + ra;
|
|
258
|
+
p[i + 1] = p[i + 1] * a + ra;
|
|
259
|
+
p[i + 2] = p[i + 2] * a + ra;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
writeBytes(writer, writeDataRLE(tempBuffer, data, channels, !!options.psb));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function writeLayerInfo(tempBuffer: Uint8Array, writer: PsdWriter, psd: Psd, globalAlpha: boolean, options: ExtendedWriteOptions) {
|
|
269
|
+
writeSection(writer, 4, () => {
|
|
270
|
+
const layers: Layer[] = [];
|
|
271
|
+
|
|
272
|
+
addChildren(layers, psd.children);
|
|
273
|
+
|
|
274
|
+
if (!layers.length) layers.push({});
|
|
275
|
+
|
|
276
|
+
writeInt16(writer, globalAlpha ? -layers.length : layers.length);
|
|
277
|
+
|
|
278
|
+
const layersData = layers.map((l, i) => getChannels(tempBuffer, l, i === 0, options));
|
|
279
|
+
|
|
280
|
+
// layer records
|
|
281
|
+
for (const layerData of layersData) {
|
|
282
|
+
const { layer, top, left, bottom, right, channels } = layerData;
|
|
283
|
+
|
|
284
|
+
writeInt32(writer, top);
|
|
285
|
+
writeInt32(writer, left);
|
|
286
|
+
writeInt32(writer, bottom);
|
|
287
|
+
writeInt32(writer, right);
|
|
288
|
+
writeUint16(writer, channels.length);
|
|
289
|
+
|
|
290
|
+
for (const c of channels) {
|
|
291
|
+
writeInt16(writer, c.channelId);
|
|
292
|
+
if (options.psb) writeUint32(writer, 0);
|
|
293
|
+
writeUint32(writer, c.length);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
writeSignature(writer, '8BIM');
|
|
297
|
+
writeSignature(writer, fromBlendMode[layer.blendMode!] || 'norm');
|
|
298
|
+
writeUint8(writer, Math.round(clamp(layer.opacity ?? 1, 0, 1) * 255));
|
|
299
|
+
writeUint8(writer, layer.clipping ? 1 : 0);
|
|
300
|
+
|
|
301
|
+
let flags = 0x08; // 1 for Photoshop 5.0 and later, tells if bit 4 has useful information
|
|
302
|
+
if (layer.transparencyProtected) flags |= 0x01;
|
|
303
|
+
if (layer.hidden) flags |= 0x02;
|
|
304
|
+
if (layer.vectorMask || (layer.sectionDivider && layer.sectionDivider.type !== SectionDividerType.Other)) {
|
|
305
|
+
flags |= 0x10; // pixel data irrelevant to appearance of document
|
|
306
|
+
}
|
|
307
|
+
if (layer.effects && hasMultiEffects(layer.effects)) { // TODO: this is not correct
|
|
308
|
+
flags |= 0x20; // just guessing this one, might be completely incorrect
|
|
309
|
+
}
|
|
310
|
+
// if ('_2' in layer) flags |= 0x20; // TEMP!!!
|
|
311
|
+
|
|
312
|
+
writeUint8(writer, flags);
|
|
313
|
+
writeUint8(writer, 0); // filler
|
|
314
|
+
writeSection(writer, 1, () => {
|
|
315
|
+
writeLayerMaskData(writer, layer, layerData);
|
|
316
|
+
writeLayerBlendingRanges(writer, psd);
|
|
317
|
+
writePascalString(writer, layer.name || '', 4);
|
|
318
|
+
writeAdditionalLayerInfo(writer, layer, psd, options);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// layer channel image data
|
|
323
|
+
for (const layerData of layersData) {
|
|
324
|
+
for (const channel of layerData.channels) {
|
|
325
|
+
writeUint16(writer, channel.compression);
|
|
326
|
+
|
|
327
|
+
if (channel.buffer) {
|
|
328
|
+
writeBytes(writer, channel.buffer);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}, true, options.psb);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function writeLayerMaskData(writer: PsdWriter, { mask }: Layer, layerData: LayerChannelData) {
|
|
336
|
+
writeSection(writer, 1, () => {
|
|
337
|
+
if (!mask) return;
|
|
338
|
+
|
|
339
|
+
const m = layerData.mask || {} as Partial<Bounds>;
|
|
340
|
+
writeInt32(writer, m.top!);
|
|
341
|
+
writeInt32(writer, m.left!);
|
|
342
|
+
writeInt32(writer, m.bottom!);
|
|
343
|
+
writeInt32(writer, m.right!);
|
|
344
|
+
writeUint8(writer, mask.defaultColor!);
|
|
345
|
+
|
|
346
|
+
let params = 0;
|
|
347
|
+
if (mask.userMaskDensity !== undefined) params |= MaskParams.UserMaskDensity;
|
|
348
|
+
if (mask.userMaskFeather !== undefined) params |= MaskParams.UserMaskFeather;
|
|
349
|
+
if (mask.vectorMaskDensity !== undefined) params |= MaskParams.VectorMaskDensity;
|
|
350
|
+
if (mask.vectorMaskFeather !== undefined) params |= MaskParams.VectorMaskFeather;
|
|
351
|
+
|
|
352
|
+
let flags = 0;
|
|
353
|
+
if (mask.disabled) flags |= LayerMaskFlags.LayerMaskDisabled;
|
|
354
|
+
if (mask.positionRelativeToLayer) flags |= LayerMaskFlags.PositionRelativeToLayer;
|
|
355
|
+
if (mask.fromVectorData) flags |= LayerMaskFlags.LayerMaskFromRenderingOtherData;
|
|
356
|
+
if (params) flags |= LayerMaskFlags.MaskHasParametersAppliedToIt;
|
|
357
|
+
|
|
358
|
+
writeUint8(writer, flags);
|
|
359
|
+
|
|
360
|
+
if (params) {
|
|
361
|
+
writeUint8(writer, params);
|
|
362
|
+
|
|
363
|
+
if (mask.userMaskDensity !== undefined) writeUint8(writer, Math.round(mask.userMaskDensity * 0xff));
|
|
364
|
+
if (mask.userMaskFeather !== undefined) writeFloat64(writer, mask.userMaskFeather);
|
|
365
|
+
if (mask.vectorMaskDensity !== undefined) writeUint8(writer, Math.round(mask.vectorMaskDensity * 0xff));
|
|
366
|
+
if (mask.vectorMaskFeather !== undefined) writeFloat64(writer, mask.vectorMaskFeather);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// TODO: handle rest of the fields
|
|
370
|
+
|
|
371
|
+
writeZeros(writer, 2);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function writeLayerBlendingRanges(writer: PsdWriter, psd: Psd) {
|
|
376
|
+
writeSection(writer, 1, () => {
|
|
377
|
+
writeUint32(writer, 65535);
|
|
378
|
+
writeUint32(writer, 65535);
|
|
379
|
+
|
|
380
|
+
let channels = psd.channels || 0; // TODO: use always 4 instead ?
|
|
381
|
+
// channels = 4; // TESTING
|
|
382
|
+
|
|
383
|
+
for (let i = 0; i < channels; i++) {
|
|
384
|
+
writeUint32(writer, 65535);
|
|
385
|
+
writeUint32(writer, 65535);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function writeGlobalLayerMaskInfo(writer: PsdWriter, info: GlobalLayerMaskInfo | undefined) {
|
|
391
|
+
writeSection(writer, 1, () => {
|
|
392
|
+
if (info) {
|
|
393
|
+
writeUint16(writer, info.overlayColorSpace);
|
|
394
|
+
writeUint16(writer, info.colorSpace1);
|
|
395
|
+
writeUint16(writer, info.colorSpace2);
|
|
396
|
+
writeUint16(writer, info.colorSpace3);
|
|
397
|
+
writeUint16(writer, info.colorSpace4);
|
|
398
|
+
writeUint16(writer, info.opacity * 0xff);
|
|
399
|
+
writeUint8(writer, info.kind);
|
|
400
|
+
writeZeros(writer, 3);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function writeAdditionalLayerInfo(writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) {
|
|
406
|
+
for (const handler of infoHandlers) {
|
|
407
|
+
let key = handler.key;
|
|
408
|
+
|
|
409
|
+
if (key === 'Txt2' && options.invalidateTextLayers) continue;
|
|
410
|
+
if (key === 'vmsk' && options.psb) key = 'vsms';
|
|
411
|
+
|
|
412
|
+
if (handler.has(target)) {
|
|
413
|
+
const large = options.psb && largeAdditionalInfoKeys.indexOf(key) !== -1;
|
|
414
|
+
|
|
415
|
+
writeSignature(writer, large ? '8B64' : '8BIM');
|
|
416
|
+
writeSignature(writer, key);
|
|
417
|
+
|
|
418
|
+
const fourBytes = key === 'Txt2' || key === 'luni' || key === 'vmsk' || key === 'artb' || key === 'artd' ||
|
|
419
|
+
key === 'vogk' || key === 'SoLd' || key === 'lnk2' || key === 'vscg' || key === 'vsms' || key === 'GdFl' ||
|
|
420
|
+
key === 'lmfx' || key === 'lrFX' || key === 'cinf' || key === 'PlLd' || key === 'Anno';
|
|
421
|
+
|
|
422
|
+
writeSection(writer, fourBytes ? 4 : 2, () => {
|
|
423
|
+
handler.write(writer, target, psd, options);
|
|
424
|
+
}, key !== 'Txt2' && key !== 'cinf' && key !== 'extn', large);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function addChildren(layers: Layer[], children: Layer[] | undefined) {
|
|
430
|
+
if (!children) return;
|
|
431
|
+
|
|
432
|
+
for (const c of children) {
|
|
433
|
+
if (c.children && c.canvas) throw new Error(`Invalid layer, cannot have both 'canvas' and 'children' properties`);
|
|
434
|
+
if (c.children && c.imageData) throw new Error(`Invalid layer, cannot have both 'imageData' and 'children' properties`);
|
|
435
|
+
|
|
436
|
+
if (c.children) {
|
|
437
|
+
layers.push({
|
|
438
|
+
name: '</Layer group>',
|
|
439
|
+
sectionDivider: {
|
|
440
|
+
type: SectionDividerType.BoundingSectionDivider,
|
|
441
|
+
},
|
|
442
|
+
// TESTING
|
|
443
|
+
// nameSource: 'lset',
|
|
444
|
+
// id: [4, 0, 0, 8, 11, 0, 0, 0, 0, 14][layers.length] || 0,
|
|
445
|
+
// layerColor: 'none',
|
|
446
|
+
// timestamp: [1611346817.349021, 0, 0, 1611346817.349175, 1611346817.3491833, 0, 0, 0, 0, 1611346817.349832][layers.length] || 0,
|
|
447
|
+
// protected: {},
|
|
448
|
+
// referencePoint: { x: 0, y: 0 },
|
|
449
|
+
});
|
|
450
|
+
addChildren(layers, c.children);
|
|
451
|
+
layers.push({
|
|
452
|
+
sectionDivider: {
|
|
453
|
+
type: c.opened === false ? SectionDividerType.ClosedFolder : SectionDividerType.OpenFolder,
|
|
454
|
+
key: fromBlendMode[c.blendMode!] || 'pass',
|
|
455
|
+
subType: 0,
|
|
456
|
+
},
|
|
457
|
+
...c,
|
|
458
|
+
});
|
|
459
|
+
} else {
|
|
460
|
+
layers.push({ ...c });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function resizeBuffer(writer: PsdWriter, size: number) {
|
|
466
|
+
let newLength = writer.buffer.byteLength;
|
|
467
|
+
|
|
468
|
+
do {
|
|
469
|
+
newLength *= 2;
|
|
470
|
+
} while (size > newLength);
|
|
471
|
+
|
|
472
|
+
const newBuffer = new ArrayBuffer(newLength);
|
|
473
|
+
const newBytes = new Uint8Array(newBuffer);
|
|
474
|
+
const oldBytes = new Uint8Array(writer.buffer);
|
|
475
|
+
newBytes.set(oldBytes);
|
|
476
|
+
writer.buffer = newBuffer;
|
|
477
|
+
writer.view = new DataView(writer.buffer);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function ensureSize(writer: PsdWriter, size: number) {
|
|
481
|
+
if (size > writer.buffer.byteLength) {
|
|
482
|
+
resizeBuffer(writer, size);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function addSize(writer: PsdWriter, size: number) {
|
|
487
|
+
const offset = writer.offset;
|
|
488
|
+
ensureSize(writer, writer.offset += size);
|
|
489
|
+
return offset;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function createThumbnail(psd: Psd) {
|
|
493
|
+
const canvas = createCanvas(10, 10);
|
|
494
|
+
let scale = 1;
|
|
495
|
+
|
|
496
|
+
if (psd.width > psd.height) {
|
|
497
|
+
canvas.width = 160;
|
|
498
|
+
canvas.height = Math.floor(psd.height * (canvas.width / psd.width));
|
|
499
|
+
scale = canvas.width / psd.width;
|
|
500
|
+
} else {
|
|
501
|
+
canvas.height = 160;
|
|
502
|
+
canvas.width = Math.floor(psd.width * (canvas.height / psd.height));
|
|
503
|
+
scale = canvas.height / psd.height;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const context = canvas.getContext('2d')!;
|
|
507
|
+
context.scale(scale, scale);
|
|
508
|
+
|
|
509
|
+
if (psd.imageData) {
|
|
510
|
+
const temp = createCanvas(psd.imageData.width, psd.imageData.height);
|
|
511
|
+
temp.getContext('2d')!.putImageData(psd.imageData, 0, 0);
|
|
512
|
+
context.drawImage(temp, 0, 0);
|
|
513
|
+
} else if (psd.canvas) {
|
|
514
|
+
context.drawImage(psd.canvas, 0, 0);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return canvas;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function getChannels(
|
|
521
|
+
tempBuffer: Uint8Array, layer: Layer, background: boolean, options: WriteOptions
|
|
522
|
+
): LayerChannelData {
|
|
523
|
+
const layerData = getLayerChannels(tempBuffer, layer, background, options);
|
|
524
|
+
const mask = layer.mask;
|
|
525
|
+
|
|
526
|
+
if (mask) {
|
|
527
|
+
let top = (mask.top as any) | 0;
|
|
528
|
+
let left = (mask.left as any) | 0;
|
|
529
|
+
let right = (mask.right as any) | 0;
|
|
530
|
+
let bottom = (mask.bottom as any) | 0;
|
|
531
|
+
let { width, height } = getLayerDimentions(mask);
|
|
532
|
+
let imageData = mask.imageData;
|
|
533
|
+
|
|
534
|
+
if (!imageData && mask.canvas && width && height) {
|
|
535
|
+
imageData = mask.canvas.getContext('2d')!.getImageData(0, 0, width, height);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (width && height && imageData) {
|
|
539
|
+
right = left + width;
|
|
540
|
+
bottom = top + height;
|
|
541
|
+
|
|
542
|
+
if (imageData.width !== width || imageData.height !== height) {
|
|
543
|
+
throw new Error('Invalid imageData dimentions');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let buffer: Uint8Array;
|
|
547
|
+
let compression: Compression;
|
|
548
|
+
|
|
549
|
+
if (RAW_IMAGE_DATA && (layer as any).maskDataRaw) {
|
|
550
|
+
// console.log('written raw layer image data');
|
|
551
|
+
buffer = (layer as any).maskDataRaw;
|
|
552
|
+
compression = Compression.RleCompressed;
|
|
553
|
+
} else if (options.compress) {
|
|
554
|
+
buffer = writeDataZipWithoutPrediction(imageData, [0]);
|
|
555
|
+
compression = Compression.ZipWithoutPrediction;
|
|
556
|
+
} else {
|
|
557
|
+
buffer = writeDataRLE(tempBuffer, imageData, [0], !!options.psb)!;
|
|
558
|
+
compression = Compression.RleCompressed;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
layerData.mask = { top, left, right, bottom };
|
|
562
|
+
layerData.channels.push({ channelId: ChannelID.UserMask, compression, buffer, length: 2 + buffer.length });
|
|
563
|
+
} else {
|
|
564
|
+
layerData.mask = { top: 0, left: 0, right: 0, bottom: 0 };
|
|
565
|
+
layerData.channels.push({ channelId: ChannelID.UserMask, compression: Compression.RawData, buffer: new Uint8Array(0), length: 0 });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return layerData;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function getLayerDimentions({ canvas, imageData }: Layer): { width: number; height: number; } {
|
|
573
|
+
return imageData || canvas || { width: 0, height: 0 };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function cropImageData(data: ImageData, left: number, top: number, width: number, height: number) {
|
|
577
|
+
const croppedData = createImageData(width, height);
|
|
578
|
+
const srcData = data.data;
|
|
579
|
+
const dstData = croppedData.data;
|
|
580
|
+
|
|
581
|
+
for (let y = 0; y < height; y++) {
|
|
582
|
+
for (let x = 0; x < width; x++) {
|
|
583
|
+
let src = ((x + left) + (y + top) * width) * 4;
|
|
584
|
+
let dst = (x + y * width) * 4;
|
|
585
|
+
dstData[dst] = srcData[src];
|
|
586
|
+
dstData[dst + 1] = srcData[src + 1];
|
|
587
|
+
dstData[dst + 2] = srcData[src + 2];
|
|
588
|
+
dstData[dst + 3] = srcData[src + 3];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return croppedData;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function getLayerChannels(
|
|
596
|
+
tempBuffer: Uint8Array, layer: Layer, background: boolean, options: WriteOptions
|
|
597
|
+
): LayerChannelData {
|
|
598
|
+
let top = (layer.top as any) | 0;
|
|
599
|
+
let left = (layer.left as any) | 0;
|
|
600
|
+
let right = (layer.right as any) | 0;
|
|
601
|
+
let bottom = (layer.bottom as any) | 0;
|
|
602
|
+
let channels: ChannelData[] = [
|
|
603
|
+
{ channelId: ChannelID.Transparency, compression: Compression.RawData, buffer: undefined, length: 2 },
|
|
604
|
+
{ channelId: ChannelID.Color0, compression: Compression.RawData, buffer: undefined, length: 2 },
|
|
605
|
+
{ channelId: ChannelID.Color1, compression: Compression.RawData, buffer: undefined, length: 2 },
|
|
606
|
+
{ channelId: ChannelID.Color2, compression: Compression.RawData, buffer: undefined, length: 2 },
|
|
607
|
+
];
|
|
608
|
+
let { width, height } = getLayerDimentions(layer);
|
|
609
|
+
|
|
610
|
+
if (!(layer.canvas || layer.imageData) || !width || !height) {
|
|
611
|
+
right = left;
|
|
612
|
+
bottom = top;
|
|
613
|
+
return { layer, top, left, right, bottom, channels };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
right = left + width;
|
|
617
|
+
bottom = top + height;
|
|
618
|
+
|
|
619
|
+
let data = layer.imageData || layer.canvas!.getContext('2d')!.getImageData(0, 0, width, height);
|
|
620
|
+
|
|
621
|
+
if (options.trimImageData) {
|
|
622
|
+
const trimmed = trimData(data);
|
|
623
|
+
|
|
624
|
+
if (trimmed.left !== 0 || trimmed.top !== 0 || trimmed.right !== data.width || trimmed.bottom !== data.height) {
|
|
625
|
+
left += trimmed.left;
|
|
626
|
+
top += trimmed.top;
|
|
627
|
+
right -= (data.width - trimmed.right);
|
|
628
|
+
bottom -= (data.height - trimmed.bottom);
|
|
629
|
+
width = right - left;
|
|
630
|
+
height = bottom - top;
|
|
631
|
+
|
|
632
|
+
if (!width || !height) {
|
|
633
|
+
return { layer, top, left, right, bottom, channels };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (layer.imageData) {
|
|
637
|
+
data = cropImageData(data, trimmed.left, trimmed.top, width, height);
|
|
638
|
+
} else {
|
|
639
|
+
data = layer.canvas!.getContext('2d')!.getImageData(trimmed.left, trimmed.top, width, height);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const channelIds = [
|
|
645
|
+
ChannelID.Color0,
|
|
646
|
+
ChannelID.Color1,
|
|
647
|
+
ChannelID.Color2,
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
if (!background || options.noBackground || layer.mask || hasAlpha(data) || (RAW_IMAGE_DATA && (layer as any).imageDataRaw?.['-1'])) {
|
|
651
|
+
channelIds.unshift(ChannelID.Transparency);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
channels = channelIds.map(channelId => {
|
|
655
|
+
const offset = offsetForChannel(channelId, false); // TODO: psd.colorMode === ColorMode.CMYK);
|
|
656
|
+
let buffer: Uint8Array;
|
|
657
|
+
let compression: Compression;
|
|
658
|
+
|
|
659
|
+
if (RAW_IMAGE_DATA && (layer as any).imageDataRaw) {
|
|
660
|
+
// console.log('written raw layer image data');
|
|
661
|
+
buffer = (layer as any).imageDataRaw[channelId];
|
|
662
|
+
compression = Compression.RleCompressed;
|
|
663
|
+
} else if (options.compress) {
|
|
664
|
+
buffer = writeDataZipWithoutPrediction(data, [offset]);
|
|
665
|
+
compression = Compression.ZipWithoutPrediction;
|
|
666
|
+
} else {
|
|
667
|
+
buffer = writeDataRLE(tempBuffer, data, [offset], !!options.psb)!;
|
|
668
|
+
compression = Compression.RleCompressed;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return { channelId, compression, buffer, length: 2 + buffer.length };
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return { layer, top, left, right, bottom, channels };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function isRowEmpty({ data, width }: PixelData, y: number, left: number, right: number) {
|
|
678
|
+
const start = ((y * width + left) * 4 + 3) | 0;
|
|
679
|
+
const end = (start + (right - left) * 4) | 0;
|
|
680
|
+
|
|
681
|
+
for (let i = start; i < end; i = (i + 4) | 0) {
|
|
682
|
+
if (data[i] !== 0) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function isColEmpty({ data, width }: PixelData, x: number, top: number, bottom: number) {
|
|
691
|
+
const stride = (width * 4) | 0;
|
|
692
|
+
const start = (top * stride + x * 4 + 3) | 0;
|
|
693
|
+
|
|
694
|
+
for (let y = top, i = start; y < bottom; y++, i = (i + stride) | 0) {
|
|
695
|
+
if (data[i] !== 0) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function trimData(data: PixelData) {
|
|
704
|
+
let top = 0;
|
|
705
|
+
let left = 0;
|
|
706
|
+
let right = data.width;
|
|
707
|
+
let bottom = data.height;
|
|
708
|
+
|
|
709
|
+
while (top < bottom && isRowEmpty(data, top, left, right))
|
|
710
|
+
top++;
|
|
711
|
+
while (bottom > top && isRowEmpty(data, bottom - 1, left, right))
|
|
712
|
+
bottom--;
|
|
713
|
+
while (left < right && isColEmpty(data, left, top, bottom))
|
|
714
|
+
left++;
|
|
715
|
+
while (right > left && isColEmpty(data, right - 1, top, bottom))
|
|
716
|
+
right--;
|
|
717
|
+
|
|
718
|
+
return { top, left, right, bottom };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function writeColor(writer: PsdWriter, color: Color | undefined) {
|
|
722
|
+
if (!color) {
|
|
723
|
+
writeUint16(writer, ColorSpace.RGB);
|
|
724
|
+
writeZeros(writer, 8);
|
|
725
|
+
} else if ('r' in color) {
|
|
726
|
+
writeUint16(writer, ColorSpace.RGB);
|
|
727
|
+
writeUint16(writer, Math.round(color.r * 257));
|
|
728
|
+
writeUint16(writer, Math.round(color.g * 257));
|
|
729
|
+
writeUint16(writer, Math.round(color.b * 257));
|
|
730
|
+
writeUint16(writer, 0);
|
|
731
|
+
} else if ('l' in color) {
|
|
732
|
+
writeUint16(writer, ColorSpace.Lab);
|
|
733
|
+
writeInt16(writer, Math.round(color.l * 10000));
|
|
734
|
+
writeInt16(writer, Math.round(color.a < 0 ? (color.a * 12800) : (color.a * 12700)));
|
|
735
|
+
writeInt16(writer, Math.round(color.b < 0 ? (color.b * 12800) : (color.b * 12700)));
|
|
736
|
+
writeUint16(writer, 0);
|
|
737
|
+
} else if ('h' in color) {
|
|
738
|
+
writeUint16(writer, ColorSpace.HSB);
|
|
739
|
+
writeUint16(writer, Math.round(color.h * 0xffff));
|
|
740
|
+
writeUint16(writer, Math.round(color.s * 0xffff));
|
|
741
|
+
writeUint16(writer, Math.round(color.b * 0xffff));
|
|
742
|
+
writeUint16(writer, 0);
|
|
743
|
+
} else if ('c' in color) {
|
|
744
|
+
writeUint16(writer, ColorSpace.CMYK);
|
|
745
|
+
writeUint16(writer, Math.round(color.c * 257));
|
|
746
|
+
writeUint16(writer, Math.round(color.m * 257));
|
|
747
|
+
writeUint16(writer, Math.round(color.y * 257));
|
|
748
|
+
writeUint16(writer, Math.round(color.k * 257));
|
|
749
|
+
} else {
|
|
750
|
+
writeUint16(writer, ColorSpace.Grayscale);
|
|
751
|
+
writeUint16(writer, Math.round(color.k * 10000 / 255));
|
|
752
|
+
writeZeros(writer, 6);
|
|
753
|
+
}
|
|
754
|
+
}
|